/* ***** BEGIN LICENSE BLOCK *****
 * Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 *
 * 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.
 *
 * The Original Code is Microsummarizer.
 *
 * The Initial Developer of the Original Code is Mozilla.
 * Portions created by the Initial Developer are Copyright (C) 2006
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *  Myk Melez <myk@mozilla.org> (Original Author)
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 *
 * ***** END LICENSE BLOCK ***** */

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

const PERMS_FILE    = 0644;
const MODE_WRONLY   = 0x02;
const MODE_TRUNCATE = 0x20;

// How often to update microsummaries, in milliseconds.
// XXX Make this a hidden pref so power users can modify it.
const UPDATE_INTERVAL = 30 * 60 * 1000; // 30 minutes

// How often to check for microsummaries that need updating, in milliseconds.
const CHECK_INTERVAL = 15 * 1000; // 15 seconds

const MICSUM_NS = new Namespace("http://www.mozilla.org/microsummaries/0.1");
const XSLT_NS = new Namespace("http://www.w3.org/1999/XSL/Transform");

//@line 55 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
const FIELD_MICSUM_GEN_URI    = "microsummary/generatorURI";
const FIELD_MICSUM_EXPIRATION = "microsummary/expiration";
const FIELD_GENERATED_TITLE   = "bookmarks/generatedTitle";
const FIELD_CONTENT_TYPE      = "bookmarks/contentType";
//@line 67 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"

function MicrosummaryService() {}

MicrosummaryService.prototype = {

//@line 73 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
  // Bookmarks Service
  __bms: null,
  get _bms() {
    if (!this.__bms)
      this.__bms = Cc["@mozilla.org/browser/nav-bookmarks-service;1"].
                   getService(Ci.nsINavBookmarksService);
    return this.__bms;
  },

  // Annotation Service
  __ans: null,
  get _ans() {
    if (!this.__ans)
      this.__ans = Cc["@mozilla.org/browser/annotation-service;1"].
                   getService(Ci.nsIAnnotationService);
    return this.__ans;
  },
//@line 116 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
 
  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  // Observer Service
  __obs: null,
  get _obs() {
    if (!this.__obs)
      this.__obs = Cc["@mozilla.org/observer-service;1"].
                   getService(Ci.nsIObserverService);
    return this.__obs;
  },

  /**
   * Make a URI from a spec.
   * @param   spec
   *          The string spec of the URI.
   * @returns An nsIURI object.
   */
  _uri: function MSS__uri(spec) {
    return this._ios.newURI(spec, null, null);
  },

//@line 166 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"

  // Directory Locator
  __dirs: null,
  get _dirs() {
    if (!this.__dirs)
      this.__dirs = Cc["@mozilla.org/file/directory_service;1"].
                   getService(Ci.nsIProperties);
    return this.__dirs;
  },

  // A cache of local microsummary generators.  This gets built on startup
  // by the _cacheLocalGenerators() method.
  _localGenerators: {},

  // The timer that periodically checks for microsummaries needing updating.
  _timer: null,

  // Interfaces this component implements.
  interfaces: [Ci.nsIMicrosummaryService, Ci.nsIObserver, Ci.nsISupports],

  // nsISupports

  QueryInterface: function MSS_QueryInterface(iid) {
    //if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
    if (!iid.equals(Ci.nsIMicrosummaryService) &&
        !iid.equals(Ci.nsIObserver) &&
        !iid.equals(Ci.nsISupportsWeakReference) &&
        !iid.equals(Ci.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  },

  // nsIObserver

  observe: function MSS_observe(subject, topic, data) {
    switch (topic) {
      case "xpcom-shutdown":
        this._destroy();
        break;
    }
  },

  _init: function MSS__init() {
    this._obs.addObserver(this, "xpcom-shutdown", true);

    // Periodically update microsummaries that need updating.
    this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
    var callback = {
      _svc: this,
      notify: function(timer) { this._svc._updateMicrosummaries() }
    };
    this._timer.initWithCallback(callback,
                                 CHECK_INTERVAL,
                                 this._timer.TYPE_REPEATING_SLACK);

    this._cacheLocalGenerators();
  },
  
  _destroy: function MSS__destroy() {
    this._timer.cancel();
    this._timer = null;
  },

  _updateMicrosummaries: function MSS__updateMicrosummaries() {
//@line 231 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
    // This try/catch block is a temporary workaround for bug 336194.
    var bookmarks;
    try {
      bookmarks = this._ans.getPagesWithAnnotation(FIELD_MICSUM_GEN_URI, {});
    }
    catch(e) {
      bookmarks = [];
    }
//@line 248 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"

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

    for ( var i = 0; i < bookmarks.length; i++ ) {
      var bookmarkID = bookmarks[i];

      // Skip this page if its microsummary hasn't expired yet.
      if (this._hasField(bookmarkID, FIELD_MICSUM_EXPIRATION) &&
          this._getField(bookmarkID, FIELD_MICSUM_EXPIRATION) > now)
        continue;

      // Create and initialize a new Microsummary instance.
      var pageURI =
//@line 262 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
        bookmarkID;
//@line 266 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
      var generatorURI = this._uri(this._getField(bookmarkID, FIELD_MICSUM_GEN_URI));
      var microsummary = new Microsummary(pageURI, generatorURI);

      // A microsummary observer that calls the microsummary service
      // to update the datastore when the microsummary finishes loading.
      var observer = {
        _svc: this,
        _bookmarkID: bookmarkID,
        onContentLoaded: function(microsummary) {
          this._svc._updateMicrosummary(this._bookmarkID, microsummary);

          // Prevent reference cycles from leaking memory.
          microsummary.removeObserver(this);
          this._bookmark = null;
          this._svc = null;
        }
      };

      // Register the observer with the microsummary and trigger
      // the microsummary to update itself.
      microsummary.addObserver(observer);
      microsummary.update();
    }
  },
  
  _updateMicrosummary: function MSS__updateMicrosummary(bookmarkID, microsummary) {
    this._setField(bookmarkID, FIELD_GENERATED_TITLE, microsummary.content);

    var now = new Date().getTime();
    this._setField(bookmarkID, FIELD_MICSUM_EXPIRATION, now + UPDATE_INTERVAL);

    LOG("updated microsummary for page " + microsummary.pageURI.spec +
        " to " + microsummary.content);
  },

  /**
   * Load local generators into the cache.
   * 
   */
  _cacheLocalGenerators: function MSS__cacheLocalGenerators() {
    // Load generators from the application directory.
    var appDir = this._dirs.get("MicsumGens", Ci.nsIFile);
    if (appDir.exists())
      this._cacheLocalGeneratorDir(appDir);

    // Load generators from the user's profile.
    var profileDir = this._dirs.get("UsrMicsumGens", Ci.nsIFile);
    if (profileDir.exists())
      this._cacheLocalGeneratorDir(profileDir);
  },

  /**
   * Load local generators from a directory into the cache.
   *
   * @param   dir
   *          nsIFile object pointing to directory containing generator files
   * 
   */
  _cacheLocalGeneratorDir: function MSS__cacheLocalGeneratorDir(dir) {
    var files = dir.directoryEntries.QueryInterface(Ci.nsIDirectoryEnumerator);
    var file = files.nextFile;

    while (file) {
      // Recursively load generators so support packs containing
      // lots of generators can organize them into multiple directories.
      if (file.isDirectory())
        this._cacheLocalGeneratorDir(file);
      else
        this._cacheLocalGeneratorFile(file);

      file = files.nextFile;
    }
    files.close();
  },

  /**
   * Load a local generator from a file into the cache.
   * 
   * @param   file
   *          nsIFile object pointing to file from which to load generator
   * 
   */
  _cacheLocalGeneratorFile: function MSS__cacheLocalGeneratorFile(file) {
    var uri = this._ios.newFileURI(file);
    var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].
                  createInstance(Ci.nsIDOMEventTarget);

    var t = this;
    request.addEventListener("load",
                             function(e) { t._localGeneratorLoadHandler(e) },
                             false);
    request = request.QueryInterface(Ci.nsIXMLHttpRequest);
    request.open("GET", uri.spec, true);
    request.send(null);
  },

  _localGeneratorLoadHandler: function(event) {
    // XXX Factor out code using getPageFromEvent().
    var request = event.target;
    var uri = request.channel.originalURI;
    var doc = request.responseXML;

    if (!doc)
      throw(uri.spec + " microsummary generator loaded, but not XML");

    if (doc.documentElement.nodeName == "parsererror")
      throw(uri.spec + " microsummary generator loaded, but invalid XML");
  
    // XXX Right now the code retrieves the first "generator" element
    // in the microsummaries namespace, regardless of whether or not
    // it's the root element.  Should it matter?
    // XXX We should assert if the file doesn't contain a generator.
    var node = doc.getElementsByTagNameNS(MICSUM_NS, "generator")[0];
    if (!node)
      return;

    var generator = new MicrosummaryGenerator();
    generator.localURI = uri;
    generator.initFromXML(node);

    // Add the generator to the local generators cache.
    // XXX Figure out why Firefox crashes on shutdown if we index generators
    // by uri.spec but doesn't crash if we index by uri.spec.split().join().
    //this._localGenerators[generator.uri.spec] = generator;
    this._localGenerators[generator.uri.spec.split().join()] = generator;

    LOG("loaded local microsummary generator\n" +
        "   local URI: " + generator.localURI.spec + "\n" +
        "  source URI: " + generator.uri.spec);
  },

  // nsIMicrosummaryService

  /**
   * Install the microsummary generator from the resource at the supplied URI.
   * Callable by content via the addMicrosummaryGenerator() sidebar method.
   *
   * @param   generatorURI
   *          the URI of the resource providing the generator
   *
   */
  addGenerator: function MSS_addGenerator(generatorURI) {
    var t = this;
    downloadXMLPage(generatorURI, function(e) { t._addGeneratorLoadHandler(e) });
  },

  _addGeneratorLoadHandler: function(event) {
    // XXX Factor out code using getPageFromEvent().
    var request = event.target;
    var uri = request.channel.originalURI;
    var doc = request.responseXML;

    if (!doc)
      throw(uri.spec + " microsummary generator loaded, but not XML");

    if (doc.documentElement.nodeName == "parsererror")
      throw(uri.spec + " microsummary generator loaded, but invalid XML");
  
    // XXX Make sure it's a valid microsummary generator.

    // Add a reference to the URI from which we got this generator so we have
    // a unique identifier for the generator and also so we can check back later
    // for updates.
    doc.documentElement.setAttribute("sourceURI", uri.spec);

    var genName = doc.documentElement.getAttribute("name");
    var fileName = sanitizeName(genName) + ".xml";
    var file = this._dirs.get("ProfD", Ci.nsIFile);
    file.append("microsummary-generators");
    file.append(fileName);
    file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);

    var fos = Cc["@mozilla.org/network/safe-file-output-stream;1"].
              createInstance(Ci.nsIFileOutputStream);
    var localFile = file.QueryInterface(Ci.nsILocalFile);
    fos.init(localFile, (MODE_WRONLY | MODE_TRUNCATE), PERMS_FILE, 0);
    var serializer = Cc["@mozilla.org/xmlextras/xmlserializer;1"].
                     createInstance(Ci.nsIDOMSerializer);
    serializer.serializeToStream(doc.documentElement, fos, null);
    if (fos instanceof Ci.nsISafeOutputStream) {
      try       { fos.finish() }
      catch (e) { fos.close()  }
    }
    else
      fos.close();

    // Finally, cache the generator in the local generators cache.
    this._cacheLocalGeneratorFile(file);
  },

  /**
   * Get the set of microsummaries available for a given page.  The set
   * might change after this method returns, since this method will trigger
   * an asynchronous load of the page in question (if it isn't already loaded)
   * to see if it references any page-specific microsummaries.
   *
   * If the caller passes a bookmark ID, and one of the microsummaries
   * is the current one for the bookmark, this method will retrieve content
   * from the datastore for that microsummary, which is useful when callers
   * want to display a list of microsummaries for a page that isn't loaded,
   * and they want to display the actual content of the selected microsummary
   * immediately (rather than after the content is asynchronously loaded).
   *
   * @param   pageURI
   *          the URI of the page for which to retrieve available microsummaries
   *
   * @param   bookmarkID (optional)
   *          the ID of the bookmark for which this method is being called
   *
   * @returns an nsIMicrosummarySet of nsIMicrosummaries for the given page
   *
   */
  getMicrosummaries: function MSS_getMicrosummaries(pageURI, bookmarkID) {
    var microsummaries = new MicrosummarySet();

    // Get microsummaries defined by local generators.
    for (var genURISpec in this._localGenerators) {
      var generator = this._localGenerators[genURISpec];

      if (generator.appliesToURI(pageURI)) {
        var microsummary = new Microsummary(pageURI, generator.uri);
        microsummary.generator = generator;

        // If this is the current microsummary for this bookmark, load the content
        // from the datastore so it shows up immediately in microsummary picking UI.
        if (bookmarkID && this.isMicrosummary(bookmarkID, microsummary))
          microsummary.content = this._getField(bookmarkID, FIELD_GENERATED_TITLE);

        microsummaries.AppendElement(microsummary);
      }
    }

    // Get microsummaries defined by the page.  If we don't have the page,
    // download it asynchronously, and then finish populating the set.
    var pageContent = getLoadedPageContent(pageURI);
    if (pageContent)
      microsummaries.extractFromPage(pageURI, pageContent);

    else {
      // Load the page with a callback that will add the page's microsummaries
      // to the set once the page has loaded.
      var callback = {
        _set: microsummaries,
        handleEvent: function(event) {
          event.target.removeEventListener("load", this, false);

          var page = getPageFromEvent(event);

          if (page.content)
            this._set.extractFromPage(page.uri, page.content);
          
          // Purge the hidden iframe we used to load the page, if any.
          if (page.container && page.container.parentNode)
            page.container.parentNode.removeChild(page.container);
        }
      };

      downloadPage(pageURI, callback);
    }

    return microsummaries;
  },

//@line 530 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
  _getField: function MSS__getField(bookmarkID, fieldName) {
    var pageURI = bookmarkID.QueryInterface(Ci.nsIURI);
    var fieldValue;

    switch(fieldName) {
    case FIELD_MICSUM_EXPIRATION:
      fieldValue = this._ans.getAnnotationInt64(pageURI, fieldName);
      break;
    case FIELD_MICSUM_GEN_URI:
    case FIELD_GENERATED_TITLE:
    case FIELD_CONTENT_TYPE:
    default:
      fieldValue = this._ans.getAnnotationString(pageURI, fieldName);
      break;
    }
    
    return fieldValue;
  },

  _setField: function MSS__setField(bookmarkID, fieldName, fieldValue) {
    var pageURI = bookmarkID.QueryInterface(Ci.nsIURI);

    switch(fieldName) {
    case FIELD_MICSUM_EXPIRATION:
      this._ans.setAnnotationInt64(pageURI,
                                   fieldName,
                                   fieldValue,
                                   0,
                                   this._ans.EXPIRE_NEVER);
      break;
    case FIELD_MICSUM_GEN_URI:
    case FIELD_GENERATED_TITLE:
    case FIELD_CONTENT_TYPE:
    default:
      this._ans.setAnnotationString(pageURI,
                                    fieldName,
                                    fieldValue,
                                    0,
                                    this._ans.EXPIRE_NEVER);
      break;
    }
  },

  _clearField: function MSS__clearField(bookmarkID, fieldName) {
    var pageURI = bookmarkID.QueryInterface(Ci.nsIURI);

    this._ans.removeAnnotation(pageURI, fieldName);
  },

  _hasField: function MSS__hasField(bookmarkID, fieldName) {
    var pageURI = bookmarkID.QueryInterface(Ci.nsIURI);
    return this._ans.hasAnnotation(pageURI, fieldName);
  },

  _getPageForBookmark: function MSS__getPageForBookmark(bookmarkID) {
    var pageURI = bookmarkID.QueryInterface(Ci.nsIURI);

    return pageURI;
  },

//@line 693 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"

  /**
   * Get the current microsummary for the given bookmark.
   *
   * @param   bookmarkID
   *          the bookmark for which to get the current microsummary
   *
   * @returns the current microsummary for the bookmark, or null
   *          if the bookmark does not have a current microsummary
   *
   */
  getMicrosummary: function MSS_getMicrosummary(bookmarkID) {
    if (!this.hasMicrosummary(bookmarkID))
      return null;

    var pageURI = this._getPageForBookmark(bookmarkID);
    var genURI = this._uri(this._getField(bookmarkID, FIELD_MICSUM_GEN_URI));
    
    var microsummary = new Microsummary(pageURI, genURI);
    return microsummary;
  },

  /**
   * Set the current microsummary for the given bookmark.
   *
   * @param   bookmarkID
   *          the bookmark for which to set the current microsummary
   *
   * @param   microsummary
   *          the microsummary to set as the current one
   *
   */
  setMicrosummary: function MSS_setMicrosummary(bookmarkID, microsummary) {
    this._setField(bookmarkID, FIELD_MICSUM_GEN_URI, microsummary.generatorURI.spec);

    // If the microsummary content has already been generated,
    // set the URI's generated title to the microsummary content
    // and expire the microsummary after the normal interval.
    if (microsummary.content) {
      var now = new Date().getTime();
      this._setField(bookmarkID, FIELD_GENERATED_TITLE, microsummary.content);
      this._setField(bookmarkID, FIELD_MICSUM_EXPIRATION, now + UPDATE_INTERVAL);
    }

    // Otherwise, expire the microsummary and update it immediately.
    else {
      if (this._hasField(bookmarkID, FIELD_MICSUM_EXPIRATION))
        this._clearField(bookmarkID, FIELD_MICSUM_EXPIRATION);
      microsummary.addObserver(this);
      microsummary.update();
    }
  },

  /**
   * Remove the current microsummary for the given bookmark.
   *
   * @param   bookmarkID
   *          the bookmark for which to remove the current microsummary
   *
   */
  removeMicrosummary: function MSS_removeMicrosummary(bookmarkID) {
    var fields = [FIELD_MICSUM_GEN_URI,
                  FIELD_MICSUM_EXPIRATION,
                  FIELD_GENERATED_TITLE,
                  FIELD_CONTENT_TYPE];

    for ( var i = 0; i < fields.length; i++ ) {
      var field = fields[i];
      if (this._hasField(bookmarkID, field))
        this._clearField(bookmarkID, field);
    }
  },

  /**
   * Whether or not the given bookmark has a current microsummary.
   *
   * @param   bookmarkID
   *          the bookmark for which to set the current microsummary
   *
   * @returns a boolean representing whether or not the given bookmark
   *          has a current microsummary
   *
   */
  hasMicrosummary: function MSS_hasMicrosummary(bookmarkID) {
    return this._hasField(bookmarkID, FIELD_MICSUM_GEN_URI);
  },

  /**
   * Whether or not the given microsummary is the current microsummary
   * for the given bookmark.
   *
   * @param   bookmarkID
   *          the bookmark to check
   *
   * @param   microsummary
   *          the microsummary to check
   *
   * @returns whether or not the microsummary is the current one
   *          for the bookmark
   *
   */
  isMicrosummary: function MSS_isMicrosummary(bookmarkID, microsummary) {
    if (!this.hasMicrosummary(bookmarkID))
      return false;

    var currentGen = this._getField(bookmarkID, FIELD_MICSUM_GEN_URI);

    if (microsummary.generatorURI.equals(this._uri(currentGen)))
      return true;

    return false
  }

};





function Microsummary(pageURI, generatorURI) {
  this._observers = [];
  this.pageURI = pageURI;
  this.generatorURI = generatorURI;
}

Microsummary.prototype = {
  interfaces: [Ci.nsIMicrosummary, Ci.nsISupports],

  // nsISupports

  QueryInterface: function (iid) {
    //if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
    if (!iid.equals(Ci.nsIMicrosummary) &&
        !iid.equals(Ci.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  },

  // nsIMicrosummary

  _content: null,
  get content() {
    // If we have everything we need to generate the content, generate it.
    if (this._content == null &&
        this.generator &&
        (this.pageContent || !this.generator.needsPageContent))
      this._content = this.generator.generateMicrosummary(this.pageContent);
  
    // Note: we return "null" if the content wasn't already generated and we
    // couldn't retrieve it from the generated title annotation or generate it
    // ourselves.  So callers shouldn't count on getting content; instead,
    // they should call update if the return value of this getter is "null",
    // setting an observer to tell them when content generation is done.
    return this._content;
  },
  set content(newValue) { this._content = newValue },

  _generatorURI: null,
  get generatorURI()         { return this._generatorURI },
  set generatorURI(newValue) { this._generatorURI = newValue },

  _generator: null,
  get generator()            { return this._generator },
  set generator(newValue)    { this._generator = newValue },

  _pageURI: null,
  get pageURI()              { return this._pageURI },
  set pageURI(newValue)      { this._pageURI = newValue },

  _pageContent: null,
  get pageContent() {
    if (!this._pageContent) {
      // If the page is currently loaded into a browser window, use that.
      var pageContent = getLoadedPageContent(this.pageURI);
      if (pageContent)
        this._pageContent = pageContent;
    }

    return this._pageContent;
  },
  set pageContent(newValue) { this._pageContent = newValue },

  // nsIMicrosummary

  _observers: null,

  addObserver: function MS_addObserver(observer) {
    // Register the observer, but only if it isn't already registered,
    // so that we don't call the same observer twice for any given change.
    if (this._observers.indexOf(observer) == -1)
      this._observers.push(observer);
  },
  
  removeObserver: function MS_removeObserver(observer) {
    //NS_ASSERT(this._observers.indexOf(observer) != -1,
    //          "can't remove microsummary observer " + observer + ": not registered");
  
    //this._observers =
    //  this._observers.filter(function(i) { observer != i });
    if (this._observers.indexOf(observer) != -1)
      this._observers.splice(this._observers.indexOf(observer), 1);
  },

  /**
   * Regenerates the microsummary, asynchronously downloading its generator
   * and content as needed.
   *
   */
  update: function MS_update() {
    LOG("microsummary.update called for page:\n  " + this.pageURI.spec +
        "\nwith generator:\n  " + this.generatorURI.spec);

    // If we don't have the generator, download it now.  After it downloads,
    // we'll re-call this method to continue updating the microsummary.
    if (!this.generator) {
      LOG("generator not yet loaded; downloading it");

      var generatorCallback = {
        _self: this,
        handleEvent: function(event) {
          this._self._generatorLoadHandler(event);
          event.target.removeEventListener("load", this, false);
          this._self = null;
        }
      };

      downloadXMLPage(this.generatorURI, generatorCallback);
      return;
    }

    // If we need the page content, and we don't have it, download it now.
    // Afterwards we'll re-call this method to continue updating the microsummary.
    if (this.generator.needsPageContent && !this.pageContent) {
      LOG("page content not yet loaded; downloading it");

      var pageCallback = {
        _self: this,
        handleEvent: function(event) {
          this._self._pageLoadHandler(event);
          event.target.removeEventListener("load", this, false);
          this._self = null;
        }
      };

      downloadPage(this.pageURI, pageCallback);
      return;
    }

    LOG("generator (and page, if needed) both loaded; generating microsummary");

    // Now that we have both the generator and (if needed) the page content,
    // generate the microsummary, then let the observers know about it.
    this.content = this.generator.generateMicrosummary(this.pageContent);
    this.pageContent = null;
    for ( var i = 0; i < this._observers.length; i++ )
      this._observers[i].onContentLoaded(this);

    LOG("generated microsummary: " + this.content);
  },

  _generatorLoadHandler: function MS__generatorLoadHandler(event) {
    var request = event.target;
    var uri = request.channel.originalURI;

    var generator = new MicrosummaryGenerator();
    generator.uri = uri;
    LOG(uri.spec + " microsummary generator downloaded");

    if (request.responseXML) {
      if (request.responseXML.documentElement.nodeName == "parsererror") {
        // XXX Figure out the parsererror format and log a specific error.
        LOG(uri.spec + " microsummary generator downloaded, but invalid XML");
        return;
      }

      generator.initFromXML(request.responseXML.documentElement);
    }
    else if (request.responseText) {
      generator.initFromText(request.responseText);
    }

    this.generator = generator;
    this.update();
  },

  _pageLoadHandler: function MS__pageLoadHandler(event) {
    var page = getPageFromEvent(event);
    this.pageContent = page.content;
    this.update();

    // Purge the hidden iframe we used to load the page, if any.
    if (page.container && page.container.parentNode)
      page.container.parentNode.removeChild(page.container);
  }

};





function MicrosummaryGenerator() {}

MicrosummaryGenerator.prototype = {

  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  interfaces: [Ci.nsIMicrosummaryGenerator, Ci.nsISupports],

  // nsISupports

  QueryInterface: function (iid) {
    //if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
    if (!iid.equals(Ci.nsIMicrosummaryGenerator) &&
        !iid.equals(Ci.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  },

  // nsIMicrosummaryGenerator

  // Normally this is just the URL from which we download the generator,
  // but for generators stored in the app or profile generators directory
  // it's the value of the generator tag's sourceURI attribute.
  _uri: null,
  get uri() { return this._uri },
  set uri(newValue) { this._uri = newValue },

  // For generators bundled with the browser or installed by the user,
  // the local URI is the URI of the local file containing the generator XML.
  _localURI: null,
  get localURI() { return this._localURI },
  set localURI(newValue) { this._localURI = newValue },

  _name: null,
  get name() { return this._name },
  set name(newValue) { this._name = newValue },

  _template: null,
  get template() { return this._template },
  set template(newValue) { this._template = newValue },

  _content: null,
  get content() { return this._content },
  set content(newValue) { this._content = newValue },

  _rules: null,

  /**
   * Determines whether or not the generator applies to a given URI.
   * By default, the generator does not apply to any URI.  In order for it
   * to apply to a URI, the URI must match one or more of the generator's
   * "include" rules and not match any of the generator's "exclude" rules.
   *
   * @param   uri
   *          the URI to test to see if this generator applies to it
   *
   * @returns boolean
   *          whether or not the generator applies to the given URI
   *
   */
  appliesToURI: function(uri) {
    var applies = false;

    for ( var i = 0 ; i < this._rules.length ; i++ ) {
      var rule = this._rules[i];

      switch (rule.type) {
      case "include":
        if (rule.regexp.test(uri.spec))
          applies = true;
        break;
      case "exclude":
        if (rule.regexp.test(uri.spec))
          return false;
        break;
      }
    }

    return applies;
  },

  get needsPageContent() {
    if (this.template)
      return true;
    else if (this.content)
      return false;
    else
      throw("needsPageContent called on uninitialized microsummary generator");
  },

  /**
   * Initializes a generator from text content.  Generators initialized
   * from text content merely return that content when their generate() method
   * gets called.
   *
   * @param   text
   *          the text content
   *
   */
  initFromText: function(text) {
    this.content = text;
  },

  /**
   * Initializes a generator from an XML description of it.
   * 
   * @param   node
   *          The XML DOM node containing the description.  This node should be
   *          a "generator" element in the microsummaries namespace.
   *
   */
  initFromXML: function(xml) {
    // XXX Make sure the argument is a DOM node, that its name is "generator",
    // and that it is in the microsummaries namespace.

    // XXX I would have wanted to retrieve the info from the XML via E4X,
    // but we'll need to pass the XSLT transform sheet to the XSLT processor,
    // and the processor can't deal with an E4X-wrapped template node.

    this.name = xml.getAttribute("name");

    // Only set the source URI from the XML if we have a local URI, i.e.
    // if this is a locally-installed generator, since for remote generators
    // the source URI of the generator is the URI from which we downloaded it.
    if (this.localURI) {
      //NS_ASSERT(xml.hasAttribute("sourceURI"), "local generator has no source URI");
      this.uri = this._ios.newURI(xml.getAttribute("sourceURI"), null, null);
    }

    // Slurp the include/exclude rules that determine the pages to which
    // this generator applies.  Order is important, so we add the rules
    // in the order in which they appear in the XML.
    this._rules = [];
    var pages = xml.getElementsByTagNameNS(MICSUM_NS, "pages")[0];
    if (pages) {
      // XXX Make sure the pages tag exists.
      for ( var i = 0; i < pages.childNodes.length ; i++ ) {
        var node = pages.childNodes[i];
        if (node.nodeType != node.ELEMENT_NODE ||
            node.namespaceURI != MICSUM_NS ||
            (node.nodeName != "include" && node.nodeName != "exclude"))
          continue;
        this._rules.push({ type: node.nodeName, regexp: new RegExp(node.textContent) });
      }
    }

    var templateNode = xml.getElementsByTagNameNS(MICSUM_NS, "template")[0];
    if (templateNode) {
      this.template = templateNode.getElementsByTagNameNS(XSLT_NS, "transform")[0]
                      || templateNode.getElementsByTagNameNS(XSLT_NS, "stylesheet")[0];
    }
      // XXX Make sure the template is a valid XSL transform sheet.
  },

  generateMicrosummary: function MSD_generateMicrosummary(pageContent) {
    if (this.content)
      return this.content.replace(/^\s+|\s+$/g, "");
    else if (this.template)
      return this._processTemplate(pageContent);
    else
      throw("generateMicrosummary called on uninitialized microsummary generator");
  },
  
  _processTemplate: function MSD__processTemplate(doc) {
    LOG("processing template " + this.template + " against document " + doc);

    // XXX Should we just have one global instance of the processor?
    var processor = Cc["@mozilla.org/document-transformer;1?type=xslt"].
                    createInstance(Ci.nsIXSLTProcessor);
    processor.importStylesheet(this.template);
    var fragment = processor.transformToFragment(doc, doc);

    LOG("template processing result: " + fragment.textContent);

    // XXX When we support HTML microsummaries we'll need to do something
    // more sophisticated than just returning the text content of the fragment.
    return fragment.textContent;
  }
};





// Microsummary sets are collections of microsummaries.  They allow callers
// to register themselves as observers of the set, and when any microsummary
// in the set changes, the observers get notified.  Thus a caller can observe
// the set instead of each individual microsummary.

function MicrosummarySet() {
  this._observers = [];
  this._elements = [];
}

MicrosummarySet.prototype = {

  // IO Service
  __ios: null,
  get _ios() {
    if (!this.__ios)
      this.__ios = Cc["@mozilla.org/network/io-service;1"].
                   getService(Ci.nsIIOService);
    return this.__ios;
  },

  interfaces: [Ci.nsIMicrosummarySet,
               Ci.nsIMicrosummaryObserver,
               Ci.nsISupports],

  QueryInterface: function (iid) {
    //if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
    if (!iid.equals(Ci.nsIMicrosummarySet) &&
        !iid.equals(Ci.nsIMicrosummaryObserver) &&
        !iid.equals(Ci.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  },

  _observers: null,
  _elements: null,

  // nsIMicrosummaryObserver

  onContentLoaded: function MSSet_onContentLoaded(microsummary) {
    for ( var i = 0; i < this._observers.length; i++ )
      this._observers[i].onContentLoaded(microsummary);
  },

  // nsIMicrosummarySet

  addObserver: function MSSet_addObserver(observer) {
    if (this._observers.length == 0) {
      for ( var i = 0 ; i < this._elements.length ; i++ )
        this._elements[i].addObserver(this);
    }

    // Register the observer, but only if it isn't already registered,
    // so that we don't call the same observer twice for any given change.
    if (this._observers.indexOf(observer) == -1)
      this._observers.push(observer);
  },
  
  removeObserver: function MSSet_removeObserver(observer) {
    //NS_ASSERT(this._observers.indexOf(observer) != -1,
    //          "can't remove microsummary observer " + observer + ": not registered");
  
    //this._observers =
    //  this._observers.filter(function(i) { observer != i });
    if (this._observers.indexOf(observer) != -1)
      this._observers.splice(this._observers.indexOf(observer), 1);
    
    if (this._observers.length == 0) {
      for ( var i = 0 ; i < this._elements.length ; i++ )
        this._elements[i].removeObserver(this);
    }
  },

  extractFromPage: function MSSet_extractFromPage(pageURI, pageContent) {
    var links = pageContent.getElementsByTagName("LINK");
    for ( var i = 0; i < links.length; i++ ) {
      var link = links[i];

      if (link.getAttribute("rel") != "microsummary")
        continue;

      // Unlike the "href" attribute, the "href" property contains
      // an absolute URI spec, so we use it here to create the URI.
      var genURI = this._ios.newURI(link.href, null, null);

      var microsummary = new Microsummary(pageURI, genURI);
      this.AppendElement(microsummary);
    }
  },

  // XXX Turn this into a complete implementation of nsICollection?
  AppendElement: function MSSet_AppendElement(element) {
    // Query the element to a microsummary.
    // XXX Should we NS_ASSERT if this fails?
    element = element.QueryInterface(Ci.nsIMicrosummary);

    if (this._elements.indexOf(element) == -1) {
      this._elements.push(element);
      element.addObserver(this);
    }

    // Notify observers that an element has been appended.
    for ( var i = 0; i < this._observers.length; i++ )
      this._observers[i].onElementAppended(element);
  },

  Enumerate: function MSSet_Enumerate() {
    return new ArrayEnumerator(this._elements);
  }
};





/**
 * An enumeration of items in a JS array.
 * @constructor
 */
function ArrayEnumerator(aItems) {
  this._index = 0;
  if (aItems) {
    for (var i = 0; i < aItems.length; ++i) {
      if (!aItems[i])
        aItems.splice(i, 1);      
    }
  }
  this._contents = aItems;
}

ArrayEnumerator.prototype = {
  interfaces: [Ci.nsISimpleEnumerator, Ci.nsISupports],

  QueryInterface: function (iid) {
    //if (!this.interfaces.some( function(v) { return iid.equals(v) } ))
    if (!iid.equals(Ci.nsISimpleEnumerator) &&
        !iid.equals(Ci.nsISupports))
      throw Components.results.NS_ERROR_NO_INTERFACE;
    return this;
  },

  _index: 0,
  _contents: [],
  
  hasMoreElements: function() {
    return this._index < this._contents.length;
  },
  
  getNext: function() {
    return this._contents[this._index++];      
  }
};





function LOG(str) {
  dump(str + "\n");
  //var css = Cc['@mozilla.org/consoleservice;1'].
  //          getService(Ci.nsIConsoleService);
  //css.logStringMessage("MsS: " + str)
}




/**
 * Get the document object for a page currently loaded into a browser window.
 * 
 * @param   pageURI
 *          the URI of the page
 *
 * @returns the document object, if the page is currently loaded
 *          into a browser window; otherwise null
 *
 */
function getLoadedPageContent(pageURI) {
  // Check open browser windows for the document, starting with the frontmost
  // window (a.k.a. the most recent one).
  var mediator = Cc["@mozilla.org/appshell/window-mediator;1"].
                 getService(Ci.nsIWindowMediator);
  // Apparently the Z order enumerator is broken on Linux.
  //var windows = mediator.getZOrderDOMWindowEnumerator("navigator:browser", true);
  var windows = mediator.getEnumerator("navigator:browser");
  while (windows.hasMoreElements()) {
    var win = windows.getNext();
    var tabBrowser = win.document.getElementById("content");
    for ( i = 0; i < tabBrowser.browsers.length; i++ ) {
      var browser = tabBrowser.browsers[i];
      if (pageURI.equals(browser.currentURI))
        return browser.contentDocument;
    }
  }

  return null;
}

/**
 * Download a page.  This function checks the content type annotation
 * for a clue how to load the file.  It calls downloadXMLPage() for XML pages
 * and downloadHTMLPage() otherwise.
 * 
 * @param   pageURI
 *          the URI of the page to download
 *
 * @param   loadHandler 
 *          the function to call when the page finishes downloading
 *
 */
function downloadPage(pageURI, loadHandler) {
  LOG(pageURI.spec + " downloading");

  var contentType;

//@line 1401 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
  var ans = Cc["@mozilla.org/browser/annotation-service;1"].
            getService(Ci.nsIAnnotationService);
  if (ans.hasAnnotation(pageURI, FIELD_CONTENT_TYPE))
    contentType = ans.getAnnotationString(pageURI, FIELD_CONTENT_TYPE);
//@line 1406 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"

  if (contentType && contentType.search(/xml$/) != -1)
    downloadXMLPage(pageURI, loadHandler);
  else
    downloadHTMLPage(pageURI, loadHandler);
}

/**
 * Download an XML page.
 * 
 * @param   pageURI
 *          the URI of the page to download
 *
 * @param   loadHandler
 *          the function to call when the page finishes downloading
 *
 */
function downloadXMLPage(pageURI, loadHandler) {
  LOG(pageURI.spec + " downloading as XML");

  var request = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance();

  if (loadHandler) {
    request = request.QueryInterface(Ci.nsIDOMEventTarget);
    request.addEventListener("load", loadHandler, false);
  }
  
  request = request.QueryInterface(Ci.nsIXMLHttpRequest);
  request.open("GET", pageURI.spec, true);
  request.send(null);
}

/**
 * Download an HTML page.
 * 
 * @param   pageURI
 *          the URI of the page to download
 *
 * @param   loadHandler 
 *          the function to call when the page finishes downloading
 *
 */
function downloadHTMLPage(pageURI, loadHandler) {
  LOG(pageURI.spec + " downloading as HTML");

  // We download HTML pages via hidden iframes in browser windows.
  // According to bz, this is the best way to do it, since bug 102699
  // is hard to fix.
  var windowMediator = Cc['@mozilla.org/appshell/window-mediator;1'].
                       getService(Ci.nsIWindowMediator);
  var window = windowMediator.getMostRecentWindow("navigator:browser");
  // XXX We can use other windows, too, so perhaps we should try
  // to get some other window if there's no browser window open.
  if (!window)
    throw(pageURI.spec + " can't download as HTML; no browser window");
  var document = window.document;
  var mainWindow = document.getElementById('main-window');

  var iframe = document.createElement('iframe');
  iframe.setAttribute("collapsed", true);
  iframe.addEventListener("load", loadHandler, true);
  mainWindow.appendChild(iframe);

  // Now that we've added the iframe to the window, we can access docShell
  // to turn off JavaScript for security and turn off images to reduce load
  // on the network.
  iframe.docShell.allowJavascript = false;
  iframe.docShell.allowImages = false;

  // Attach the URI to the iframe so we can retrieve it later.
  iframe.originalURI = pageURI;

  // Kick off the download.
  iframe.setAttribute('src', pageURI.spec);
}

/**
 * Get the original URI and the document object from a page load event.
 * Abstracts away the differences between the XMLHttpRequest and hidden iframe
 * approaches to loading pages.
 *
 * @param   event
 *          the page load event from which to get the URI and doc object
 *
 * @returns A page object containing the page URI, content, and container.
 *          The container is the hidden iframe the page was loaded in; we have
 *          to pass that back so the caller can delete it after processing
 *          the document, since the document would get screwed up if we deleted
 *          the iframe here.
 *
 */
function getPageFromEvent(event) {
  var page = {};

  if (event.target.channel) {
    // this was an XMLHttpRequest page load

    var request = event.target;
    page.uri = request.channel.originalURI;

    if (!request.responseXML) {
      throw(page.uri.spec + " page downloaded as XML, but not XML");
      // XXX Update the page's content type annotation
      // XXX Do something about pages with an XML content type
      // but which aren't XML?
    }
    
    page.content = request.responseXML;

    if (page.content.documentElement.nodeName == "parsererror") {
      throw(page.uri.spec + " page downloaded, but invalid XML");
      // XXX Figure out the parsererror format and log a specific error.
    }
  }
  else if (event.currentTarget && event.currentTarget.nodeName &&
           event.currentTarget.nodeName == "iframe") {
    // this was a hidden iframe page load

    // Make sure this event doesn't reach any other load handlers
    // intended for visible document frames in the browser window.
    event.preventDefault();
    event.stopPropagation();

    var iframe = event.currentTarget;

    page.uri = iframe.originalURI;
    page.content = iframe.contentDocument;
    page.container = iframe;
  }

//@line 1537 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"
  // This is as good a time as any to update the page's content type
  // in the annotations database.  XXX But should we only do this for pages
  // being summarized?  If we do it here, we'll do it for generators too.
  var ans = Cc["@mozilla.org/browser/annotation-service;1"].
            getService(Ci.nsIAnnotationService);
  ans.setAnnotationString(page.uri,
                          FIELD_CONTENT_TYPE,
                          page.content.contentType,
                          0,
                          ans.EXPIRE_NEVER);
//@line 1548 "/cygdrive/c/builds/tinderbox/Fx-Trunk-Cairo/WINNT_5.2_Depend/mozilla/browser/components/microsummaries/src/nsMicrosummaryService.js.in"

  return page;
}





// From http://lxr.mozilla.org/mozilla/source/browser/components/search/nsSearchService.js

/**
 * Removes all characters not in the "chars" string from aName.
 *
 * @returns a sanitized name to be used as a filename, or a random name
 *          if a sanitized name cannot be obtained (if aName contains
 *          no valid characters).
 */
function sanitizeName(aName) {
  const chars = "-abcdefghijklmnopqrstuvwxyz0123456789";

  var name = aName.toLowerCase();
  name = name.replace(/ /g, "-");
  //name = name.split("").filter(function (el) {
  //                               return chars.indexOf(el) != -1;
  //                             }).join("");
  var filteredName = "";
  for ( var i = 0 ; i < name.length ; i++ )
    if (chars.indexOf(name[i]) != -1)
      filteredName += name[i];
  name = filteredName;

  if (!name) {
    // Our input had no valid characters - use a random name
    for (var i = 0; i < 8; ++i)
      name += chars.charAt(Math.round(Math.random() * (chars.length - 1)));
  }

  return name;
}





var gModule = {
  registerSelf: function(componentManager, fileSpec, location, type) {
    componentManager = componentManager.QueryInterface(Ci.nsIComponentRegistrar);
    
    for (var key in this._objects) {
      var obj = this._objects[key];
      componentManager.registerFactoryLocation(obj.CID,
                                               obj.className,
                                               obj.contractID,
                                               fileSpec,
                                               location,
                                               type);
    }
  },
  
  unregisterSelf: function(componentManager, fileSpec, location) {},

  getClassObject: function(componentManager, cid, iid) {
    if (!iid.equals(Components.interfaces.nsIFactory))
      throw Components.results.NS_ERROR_NOT_IMPLEMENTED;
  
    for (var key in this._objects) {
      if (cid.equals(this._objects[key].CID))
      return this._objects[key].factory;
    }
    
    throw Components.results.NS_ERROR_NO_INTERFACE;
  },
  
  _objects: {
    service: {
      CID        : Components.ID("{460a9792-b154-4f26-a922-0f653e2c8f91}"),
      contractID : "@mozilla.org/microsummary/service;1",
      className  : "Microsummary Service",
      factory    : MicrosummaryServiceFactory = {
                     createInstance: function(aOuter, aIID) {
                       if (aOuter != null)
                         throw Components.results.NS_ERROR_NO_AGGREGATION;
                       var svc = new MicrosummaryService();
                       svc._init();
                      return svc.QueryInterface(aIID);
                     }
                   }
    }
  },
  
  canUnload: function(componentManager) {
    return true;
  }
};

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