// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2009
// 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 CLASS_ID                = Components.ID("{C2BFF231-7A51-4764-8409-A1B22B2F5147}");
const CLASS_NAME              = "Flock Indexer";
const CONTRACT_ID             = "@flock.com/indexer;1";

Components.utils.import("resource:///modules/FlockStringBundleHelpers.jsm");

const FLOCK_NS = "http://flock.com/rdf#";
const NSCP_NS = "http://home.netscape.com/NC-rdf#";

const PROP_CHILD = NSCP_NS + "child";

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

// Annotation used by Places to store bookmarks' descriptions
const DESCRIPTION_ANNO = "bookmarkProperties/description";

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

const OP_DELETE      = 0;
const OP_ADD_FLOCK   = 1;
const OP_ADD_HISTORY = 2;
const OP_ADD_PLACE   = 3;
const OP_CLEAR       = 4;

function flockIndexer() {
  this._initialized = false;

  var obs = Cc["@mozilla.org/observer-service;1"].getService(Ci.nsIObserverService);
  obs.addObserver(this, "profile-change-teardown", false);
  obs.addObserver(this, "profile-change-teardown-veto", false);
  obs.addObserver(this, "xpcom-shutdown", false);
}

flockIndexer.prototype = {

  init: function INDEXER_init() {
    this._initialized = true;

    this._enabled = false;
    this._enabledWebHistory = false;
    
    this._logger = Cc["@flock.com/logger;1"].createInstance(Ci.flockILogger);
    this._logger.init("indexer");
    
    this._logger.info("starting up...");
    
    this._profiler = Cc["@flock.com/profiler;1"].getService(Ci.flockIProfiler);

    this._RDFS = Cc["@mozilla.org/rdf/rdf-service;1"]
      .getService(Ci.nsIRDFService);
    this._resIsIndexable = this._RDFS.GetResource(FLOCK_NS + "isIndexable");
    this._resTitle = this._RDFS.GetResource(NSCP_NS + "Name");
    this._resDescription = this._RDFS.GetResource(NSCP_NS + "Description");
    this._resTags = this._RDFS.GetResource(FLOCK_NS + "tags");

    this._favService = Cc["@mozilla.org/rdf/datasource;1?name=flock-favorites"]
      .getService(Ci.flockIRDFObservable);
    this._historyService = Cc["@mozilla.org/browser/nav-history-service;1"]
                           .getService(Ci.nsINavHistoryService);
    this._bookmarksService = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]
                             .getService(Ci.nsINavBookmarksService);
    this._livemarkService = Cc["@mozilla.org/browser/livemark-service;2"]
                            .getService(Ci.nsILivemarkService);
    this._annosvc = Cc["@mozilla.org/browser/annotation-service;1"]
                    .getService(Ci.nsIAnnotationService);

    this._ios = Cc["@mozilla.org/network/io-service;1"]
      .getService(Ci.nsIIOService);

    this._coop = Cc["@flock.com/singleton;1"]
                 .getService(Ci.flockISingleton)
                 .getSingleton("chrome://flock/content/common/load-faves-coop.js")
                 .wrappedJSObject;

    // initialize the queue
    this._initQueue();

    // init queue processing timer
    this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);

    // Process any saved queue items.
    this._timer.initWithCallback(this, 15000, Ci.nsITimer.TYPE_ONE_SHOT);

    // init page text store
    this._pageText = {};
    
    // create tokenizer
    this._tokenizer = Cc["@flock.com/tokenizer;1"].createInstance(Ci.flockITokenizer);
    
    // init with current prefs
    this.observe(null, "nsPref:changed", null);
    
    // watch for pref changes
    var prefService = Cc["@mozilla.org/preferences-service;1"]
      .getService(Ci.nsIPrefBranch2);
    prefService.addObserver("flock.service.indexer.enabled", this, false);
    prefService.addObserver("flock.service.indexer.indexWebHistory", this, false);
  },
  
  rebuildIndex: function INDEXER_rebuildIndex() {
    this._indexFavorites();
    this._indexHistoryNode(this._getHistoryRoot());
  },

  _indexFavorites: function INDEXER__rebuildFavoritesIndex() {
    this._logger.info("Reindexing local favorites...");
    this._indexBookmarkFolder(this._bookmarksService.toolbarFolder);
    this._indexBookmarkFolder(this._bookmarksService.bookmarksMenuFolder);
    this._indexBookmarkFolder(this._bookmarksService.unfiledBookmarksFolder);
  },

  _getHistoryRoot: function INDEXER__getHistoryRoot() {
    var query = this._historyService.getNewQuery();
    var options = this._historyService.getNewQueryOptions();
    return this._historyService.executeQuery(query, options).root;
  },

  _rebuildIndex: function INDEXER__rebuildIndex(aMigrationProgressListener) {
    // reindex favorites
    aMigrationProgressListener.onUpdate(10, "Reindexing local favorites");
    yield false;
    this._indexFavorites();

    aMigrationProgressListener.onUpdate(20, "Reindexing online favorites");
    yield false;
    var onlineRoot = this._annosvc.getItemsWithAnnotation("flock/internalroot",
                                                          {});
    this._indexBookmarkFolder(onlineRoot);
    // reindex history
    var histRoot = this._getHistoryRoot();
    histRoot.containerOpen = true;
    var oldPercent = 0;
    for (var i = 0; i < histRoot.childCount; i++) {
      var percent = 30 + Math.round(i / histRoot.childCount * 70);
      if (oldPercent != percent) {
        oldPercent = percent;
        aMigrationProgressListener.onUpdate(percent, "Reindexing history");
        yield false;
      }
      this._indexHistoryNode(histRoot.getChild(i));
    }
    yield true;
  },

  _indexBookmarkFolder: function INDEXER__indexBookmarkFolder(folderId) {
    var query = this._historyService.getNewQuery();
    var options = this._historyService.getNewQueryOptions();
    query.setFolders([folderId], 1);
    var result = this._historyService.executeQuery(query, options);
    var rootNode = result.root;

    rootNode.containerOpen = true;
    for (var i = 0; i < rootNode.childCount; i ++) {
      var node = rootNode.getChild(i);

      switch(node.type) {
        case node.RESULT_TYPE_URI:
          this._addOp(OP_ADD_PLACE, "place:" + node.itemId);
          break;
        case node.RESULT_TYPE_FOLDER:
          if (!this._livemarkService.isLivemark(node.itemId)) {
            // Don't index livemark items
            this._indexBookmarkFolder(node.itemId);
          }
          break;
      }
    }
  },

  _indexHistoryNode: function INDEXER__indexHistoryNode(aHistoryResultNode) {
    var node = aHistoryResultNode;

    switch(node.type) {
      case node.RESULT_TYPE_URI:
      case node.RESULT_TYPE_VISIT:
      case node.RESULT_TYPE_FULL_VISIT:
        this._addOp(OP_ADD_HISTORY, "history:" + node.uri);
        break;
      case node.RESULT_TYPE_FOLDER:
      case node.RESULT_TYPE_QUERY:
        node.containerOpen = true;
        if (node instanceof Ci.nsINavHistoryContainerResultNode &&
            node.hasChildren)
        {
          for (var i = 0; i < node.childCount; i++) {
            this._indexHistoryNode(node.getChild(i));
          }
        }
        break;
    }
  },

  _clear: function INDEXER__clear(aType) {
    var inst = this;

    // Remove any related operation from the queue
    var newQueue = [];
    for (var i = 0; i < this._queue.length; i++) {
      var op = this._queue[i];
      if (op[1].substring(0, aType.length) != aType) {
        newQueue.push(op);
      }
    }
    this._queue = newQueue;

    // Clear the index
    function searchCallback(aCount, aEnumerator) {
      while (aEnumerator.hasMoreElements()) {
        var entry = aEnumerator.getNext();
        if (!entry) continue;
        inst._addOp(OP_DELETE, entry.URI);
      }
    };

    this._searchService.search(null, aType, -1, searchCallback);
  },

  _getQueueFile: function INDEXER__getQueueFile() {
    var file = Cc["@mozilla.org/file/directory_service;1"]
      .getService(Ci.nsIProperties).get("ProfD", Ci.nsILocalFile);
    file.append("indexerQueue.js");
    return file;
  },
  
  _initQueue: function INDEXER__initQueue() {
    var queue;

    try {
      var file = this._getQueueFile();

      var stream = Cc["@mozilla.org/network/file-input-stream;1"]
        .createInstance(Ci.nsIFileInputStream);
      stream.init(file, PR_RDONLY, 0, 0);

      var cvstream = Cc["@mozilla.org/intl/converter-input-stream;1"]
        .createInstance(Ci.nsIConverterInputStream);
      cvstream.init(stream, "UTF-8", 1024,
                    Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);

      var content = "";
      var data = {};
      while (cvstream.readString(4096, data)) {
        content += data.value;
      }
      cvstream.close();

      queue = content.replace(/\r\n?/g, "\n");
      file.remove(false);
    } catch (e) {
      queue = null;
    }

    if (queue) {
      try {
        var nsJSON = Cc["@mozilla.org/dom/json;1"].createInstance(Ci.nsIJSON);
        this._queue = nsJSON.decode(queue);
        this._logger.info("restored queue");
        return;
      } catch (e) {
        this._logger.error("unable to restore queue");
      }
    }
    
    // default to empty queue
    this._queue = [];
  },
  
  _saveQueue: function INDEXER__saveQueue() {
    // do nothing if queue is empty
    if (this._queue.length == 0)
      return;
    
    try {
      this._logger.info("saving queue...");
      var file = this._getQueueFile();

      var ostream = Cc["@mozilla.org/network/safe-file-output-stream;1"]
        .createInstance(Ci.nsIFileOutputStream);
      ostream.init(file, PR_WRONLY | PR_CREATE_FILE | PR_TRUNCATE, 0600, 0);

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

      var data = this._queue.toSource();
      var convdata = converter.ConvertFromUnicode(data) + converter.Finish();

      ostream.write(convdata, convdata.length);

      if (ostream instanceof Ci.nsISafeOutputStream) {
        ostream.finish();
      } else {
        ostream.close();
      }
    } catch (e) {
      this._logger.error("unable to save queue");
    }
  },
  
  _enable: function INDEXER__enable() {
    if (!this._enabled) {
      // start watching the favorites and history graphs
      this._favService.addArcObserver(Ci.flockIRDFObserver.TYPE_ALL, null,
                                      this._resIsIndexable, null, this);
      this._favService.addArcObserver(Ci.flockIRDFObserver.TYPE_CHANGE, null,
                                      this._resTitle, null, this);
      this._favService.addArcObserver(Ci.flockIRDFObserver.TYPE_CHANGE, null,
                                      this._resDescription, null, this);
      this._favService.addArcObserver(Ci.flockIRDFObserver.TYPE_CHANGE, null,
                                      this._resTags, null, this);
      this._historyService.addObserver(this, false);
      this._bookmarksService.addObserver(this, false);
      this._enabled = true;
      this._enabledWebHistory = true;
      this._logger.info("indexing enabled");
    }
  },
  
  _disable: function INDEXER__disable() {
    if (this._enabled) {
      // stop watching the favorites and history graphs
      this._favService.removeArcObserver(Ci.flockIRDFObserver.TYPE_ALL, null,
                                         this._resIsIndexable, null, this);
      this._favService.removeArcObserver(Ci.flockIRDFObserver.TYPE_CHANGE, null,
                                      this._resTitle, null, this);
      this._favService.removeArcObserver(Ci.flockIRDFObserver.TYPE_CHANGE, null,
                                      this._resDescription, null, this);
      this._favService.removeArcObserver(Ci.flockIRDFObserver.TYPE_CHANGE, null,
                                      this._resTags, null, this);
      this._historyService.removeObserver(this);
      this._bookmarksService.removeObserver(this);
      this._enabled = false;
      this._enabledWebHistory = false;
      this._logger.info("indexing disabled");
    }
  },

  _enableWebHistory: function INDEXER__enableWebHistory() {
    if (this._enabled) {
      if (!this._enabledWebHistory) {
        this._historyService.addObserver(this, false);
        this._enabledWebHistory = true;
        this._logger.info("web history indexing enabled");
      }
    }
  },
  
  _disableWebHistory: function INDEXER__disableWebHistory() {
    if (this._enabled) {
      if (this._enabledWebHistory) {
        this._historyService.removeObserver(this);
        this._enabledWebHistory = false;
        this._logger.info("web history indexing disabled");
      }
    }
  },
  
  _shutdown: function INDEXER__shutdown() {
    if (this._initialized) {
      this._disable();
      this._timer.cancel();
      this._saveQueue();
      this._logger.info("shut down");
      this._initialized = false;
    }
  },
  
  _processQueue: function INDEXER__processQueue(sync) {
    if (!this._searchService) {
      var dirService = Cc["@mozilla.org/file/directory_service;1"]
        .getService(Ci.nsIProperties);
      var profileDir = dirService.get("ProfD", Ci.nsIFile);

      // Clean up old cardinal index dir
      var oldHistorySearchDir = profileDir.clone();
      oldHistorySearchDir.append("historysearch");
      try {
        oldHistorySearchDir.remove(true);
      }
      catch (ex) {
      }

      // Initialize Lucene
      var luceneDir = profileDir.clone();
      luceneDir.append("lucene");
      this._searchService = Cc["@flock.com/lucene;1"]
                            .getService(Ci.flockILucene);
      this._searchService.init(luceneDir);

      this._tagsvc = Cc["@mozilla.org/browser/tagging-service;1"]
                     .getService(Ci.nsITaggingService);

    }
    var batchSize = 1;
    var c = 0;
    while (this._queue.length > 0 && c < batchSize) {
      c++;
      var op = this._queue[0];
      var instruction = op[0];
      var uri = op[1];
      switch (instruction) {
        case OP_DELETE:
          this._logger.info("removing " + uri);
          if (sync) {
            this._searchService.deleteDocumentSync(uri);
          } else {
            this._searchService.deleteDocument(uri, this);
          }
          break;

        case OP_CLEAR:
          this._logger.info("clear all items of type " + uri);
          this._clear(uri);
          break;

        case OP_ADD_FLOCK:
          var obj = this._coop.get(uri);
          if (!obj) {
            var msg = "trying to add nonexistent object: " + uri;
            this._logger.error(msg);
            return;
            //throw Components.Exception(msg, Cr.NS_ERROR_UNEXPECTED);
          }

          var url = obj.URL;
          if (!url) {
             this._logger.warn("unable to get URL for " + uri);
             url = "";
          }

          var type = obj.flockType;
          var title = obj.name;
          if (obj.tags) {
            var tags = obj.tags;
          } else {
            var tags = "";
          }
          if (obj.description) {
            var description = obj.description;
          } else {
            var description = "";
          }
          var text = "";
          this._logger.info("indexing " + uri + " type: " + type + " title: " + title + " url: " + url);

          if (sync) {
            this._searchService.addDocumentSync(uri, type, url, title, tags, description, text);
          } else {
            this._searchService.addDocument(uri, type, url, title, tags, description, text, this);
          }
          break;

        case OP_ADD_HISTORY:
          this._logger.info("indexing " + uri + " type: history");
          if (!sync) {
            this._addHistoryDocument(uri);
          }
          break;

        case OP_ADD_PLACE:
          if (!sync) {
            var url = this._addPlacesItem(uri);
            this._logger.info("indexing " + uri + "-" + url);
          }
          break;

      }
    }
  },

  _addHistoryDocument: function INDEXER__addHistoryDocument(aURI) {
    var uri = aURI;
    var url = uri.substr("history:".length);
    var tags = "";
    var description = "";

    var query = this._historyService.getNewQuery();
    var options = this._historyService.getNewQueryOptions();
    query.uri = this._ios.newURI(url, null, null);

    var queryResult = this._historyService.executeQuery(query, options);
    var resultsRoot = queryResult.root;
    resultsRoot.containerOpen = true;
    var data = this._pageText[url];
    delete this._pageText[url];
    if (resultsRoot.childCount == 0) {
      // Pages without a title are usually advertisements
      this._retireOp();
      return;
    }
    var title = resultsRoot.getChild(0).title;
    this._searchService.addDocument(uri, "history", url, title, tags, description, data, this);
    this._logger.debug("Indexed characters: " + (data ? data.length : 0));
  },

  _addPlacesItem: function INDEXER__addPlacesItem(aURI) {
    var placesId = parseInt(aURI.substr("place:".length), 10);

    try {
      var pageURI = this._bookmarksService.getBookmarkURI(placesId);
    } catch (ex) {
      this._logger.info("Couldn't index favorite, placeid="
                        + placesId
                        + " is invalid");
      this._retireOp();
      return;
    }
    var url = pageURI.spec;
    var title = this._bookmarksService.getItemTitle(placesId);
    var description = "";
    try {
      description = this._annosvc.getItemAnnotation(placesId, DESCRIPTION_ANNO);
    } catch(e) {
      // No description.
      // We could check with itemHasAnnotation before but that would
      // generate one extra query. Try/catch is more efficient.
    }
    var tags = this._tagsvc.getTagsForURI(pageURI, {}).join(" ");

    this._searchService.addDocument(aURI,
                                    "bookmark",
                                    url,
                                    title,
                                    tags,
                                    description,
                                    "" /* data */,
                                    this);
    return url;
  },

  _addOp: function INDEXER__addOp(aOp, aURI) {
    if (!this._queue.length) {
      this._timer.initWithCallback(this, 1000, Ci.nsITimer.TYPE_ONE_SHOT);
    }
    this._queue.push([aOp, aURI]);
    this._logger.info("queued operation op: " + aOp + " uri: " + aURI);
  },
  
  _retireOp: function INDEXER__retireOp() {
    this._queue.shift();
    if (this._queue.length > 0) {
      this._timer.initWithCallback(this, 200, Ci.nsITimer.TYPE_ONE_SHOT);
    }
  },
  
  _handlePageLoad: function INDEXER_handlePageLoad(document) {
    var profilerEvt = this._profiler.profileEventStart("indexer-tokenize");
    var url = document.documentURI;
    this._logger.debug("_handlePageLoad: " + url);
    if (document.body) {
      this._pageText[url] = this._tokenizer.tokenizeDOMNode(document,
                                                            document.body);
    }
    this._addOp(OP_ADD_HISTORY, "history:" + url);
    this._profiler.profileEventEnd(profilerEvt, url);
  },
  
  // nsIDOMEventListener
  handleEvent: function INDEXER_handleEvent(aEvent) {
    switch(aEvent.type) {
      case "load":
        var url;
        try {
          url = this._ios.newURI(aEvent.originalTarget.documentURI, null, null);
        } catch (e) { }
        if (url && url.scheme == "http") {
          this._handlePageLoad(aEvent.originalTarget);
        }
        break;
    }
  },

  // flockIMigratable
  get migrationName() {
    return flockGetString("common/migrationNames", "migration.name.indexer");
  },
  
  // flockIMigratable
  needsMigration: function INDEXER_needsMigration(oldVersion) {
    var versionChecker = Cc["@mozilla.org/xpcom/version-comparator;1"]
                         .getService(Ci.nsIVersionComparator);
    return (versionChecker.compare(oldVersion, "2.5") < 0);
  },
  
  // flockIMigratable
  startMigration: function INDEXER_startMigration(oldVersion, aListener) {
    var versionChecker = Cc["@mozilla.org/xpcom/version-comparator;1"]
                         .getService(Ci.nsIVersionComparator);
    var indexRebuilder = null;
    if (versionChecker.compare(oldVersion, "2.5") < 0) {
      // Due to a large number of startup crashes caused by corrupt
      // lucene indexes, we're deleting the index on upgrade to 2.5
      var dirService = Cc["@mozilla.org/file/directory_service;1"]
                       .getService(Ci.nsIProperties);
      var luceneDir = dirService.get("ProfD", Ci.nsIFile);
      luceneDir.append("lucene");
      try {
        luceneDir.remove(true);
      } catch (ex) {
        // This can happen if the dir didn't exist
      }

      // favorites from 1.x have to be imported to sqlite -> triggers reindexing
      if (versionChecker.compare(oldVersion, "2.0") >= 0) {
        indexRebuilder = this._rebuildIndex(aListener);
      }
    }
    return { wrappedJSObject: { indexRebuilder: indexRebuilder } };
  },
  
  // flockIMigratable
  finishMigration: function INDEXER_finishMigration(ctxtWrapper) {
  },
  
  // flockIMigratable
  doMigrationWork: function INDEXER_doMigrationWork(ctxtWrapper) {
    var ctxt = ctxtWrapper.wrappedJSObject;
    if (ctxt.indexRebuilder) {
      return !ctxt.indexRebuilder.next();
    }
    return false;
  },
  
  // flockILuceneIndexListener
  onAddDocumentComplete: function INDEXER_onAddDocumentComplete(aURI) {
    this._logger.debug("onAddDocumentComplete: " + aURI);
    this._retireOp();
  },

  // flockILuceneIndexListener
  onDeleteDocumentComplete: function INDEXER_onDeleteDocumentComplete(aURI) {
    this._logger.debug("onDeleteDocumentComplete: " + aURI);
    this._retireOp();
  },

  // nsITimerCallback
  notify: function INDEXER_notify(timer) {
    this._processQueue(false);
  },

  // flockIRDFObserver
  rdfChanged: function INDEXER__rdfChanged(ds, type, rsrc, pred, obj, oldObj) {
    if (pred == this._resIsIndexable) {
      var isIndexable = obj.QueryInterface(Ci.nsIRDFLiteral).Value == "true";
      switch (type) {
        case Ci.flockIRDFObserver.TYPE_ASSERT:
          if (isIndexable) {
            rsrc.QueryInterface(Ci.nsIRDFResource);
            this._addOp(OP_ADD_FLOCK, rsrc.ValueUTF8);
          }
          break;
  
        case Ci.flockIRDFObserver.TYPE_CHANGE:
          var op = isIndexable ? OP_ADD_FLOCK : OP_DELETE;
          rsrc.QueryInterface(Ci.nsIRDFResource);
          this._addOp(op, rsrc.ValueUTF8);
          break;
  
        case Ci.flockIRDFObserver.TYPE_UNASSERT:
          if (isIndexable) {
            rsrc.QueryInterface(Ci.nsIRDFResource);
            this._addOp(OP_DELETE, rsrc.ValueUTF8);
          }
          break;
      }
    } else {
      var indexable = this._coop.get_from_resource(rsrc);
      if (indexable && indexable.isIndexable) {
        this._addOp(OP_ADD_FLOCK, indexable.id());
      }
    }
  },

  // nsINavHistoryObserver
  onPageChanged: function INDEXER_onPageChanged(aURI, aWhat, aValue) {
    if (aURI.scheme == "http" &&
        aWhat != Ci.nsINavHistoryObserver.ATTRIBUTE_FAVICON)
    {
      this._addOp(OP_ADD_HISTORY, "history:" + aURI.spec);
    }
  },
  
  // nsINavHistoryObserver
  onDeleteURI: function INDEXER_onUnassert(aURI) {
    if (aURI.scheme == "http") {
      this._addOp(OP_DELETE, "history:" + aURI.spec);
    }
  },

  // nsINavHistoryObserver
  onClearHistory: function INDEXER_onClearHistory() {
    this._addOp(OP_CLEAR, "history");
  },

  // nsINavHistoryObserver
  onPageExpired: function INDEXER_onPageExpired(aURI, aVisitTime, aWholeEntry) {
    // no-op
  },

  // nsINavHistoryObserver, nsINavBookmarkObserver
  onBeginUpdateBatch: function INDEXER_onBeginUpdateBatch() {
    // no-op
  },

  // nsINavHistoryObserver, nsINavBookmarkObserver
  onEndUpdateBatch: function INDEXER_onEndUpdateBatch() {
    // no-op
  },

  // nsINavHistoryObserver
  onTitleChanged: function INDEXER_onTitleChanged(aURI, aPageTitle) {
    // no-op
  },

  // nsINavHistoryObserver
  onVisit: function INDEXER_onVisit(aURI,
                                    aVisitID,
                                    aTime,
                                    aSessionID,
                                    aRefID,
                                    aTransType)
  {
    // no-op
  },

  // nsINavBookmarkObserver
  onItemAdded: function INDEXER_onItemAdded(aItemId, aFolder, aIndex) {
    var type = this._bookmarksService.getItemType(aItemId);
    if (type == this._bookmarksService.TYPE_BOOKMARK) {
      this._addOp(OP_ADD_PLACE, "place:" + aItemId);
    }
  },

  // nsINavBookmarkObserver
  onItemRemoved: function INDEXER_onItemRemoved(aItemId, aFolder, aIndex) {
    this._addOp(OP_DELETE, "place:" + aItemId);
  },

  // nsINavBookmarkObserver
  onItemChanged: function INDEXER_onItemChanged(aItemId,
                                                aProperty,
                                                aIsAnnotationProperty,
                                                aValue) {
    var type = this._bookmarksService.getItemType(aItemId);
    if (type == this._bookmarksService.TYPE_BOOKMARK &&
        aProperty && aProperty != "favicon") 
    {
      this._addOp(OP_ADD_PLACE, "place:" + aItemId + " prop: " + aProperty);
    }
  },

  // nsINavBookmarkObserver
  onItemVisited: function INDEXER_onItemVisited(aBookmarkId,
                                                aVisitID,
                                                time) {
    // no-op
  },

  // nsINavBookmarkObserver
  onItemMoved: function INDEXER_onItemMoved(aItemId,
                                            aOldParent,
                                            aOldIndex,
                                            aNewParent,
                                            aNewIndex) {
    // no-op
  },

  // nsIObserver
  observe: function INDEXER_observe(subject, topic, state) {
    switch (topic) {
      case "xpcom-shutdown":
        var obs = Cc["@mozilla.org/observer-service;1"]
          .getService(Ci.nsIObserverService);
        obs.removeObserver(this, "xpcom-shutdown");
        obs.removeObserver(this, "profile-change-teardown");
        obs.removeObserver(this, "profile-change-teardown-veto");
        // fall through
      case "profile-change-teardown":
        this._shutdown();
        return;
      case "profile-change-teardown-veto":
        this.init();
        return;
      case "nsPref:changed":
        var prefService = Cc["@mozilla.org/preferences-service;1"]
          .getService(Ci.nsIPrefBranch);
        if (prefService.getPrefType("flock.service.indexer.enabled")) {
          if (prefService.getBoolPref("flock.service.indexer.enabled")) {
            this._enable();
          } else {
            this._disable();
          }
        } else {
          this._enable();
        }
        if (prefService.getPrefType("flock.service.indexer.indexWebHistory")) {
          if (prefService.getBoolPref("flock.service.indexer.indexWebHistory")) {
            this._enableWebHistory();
          } else {
            this._disableWebHistory();
          }
        } else {
          this._enableWebHistory();
        }
        break;
    }
  },
  
  // nsIClassInfo
  getInterfaces: function INDEXER_getInterfaces(aCount) {
    var interfaces = [Ci.flockIIndexer, Ci.nsIClassInfo, Ci.nsIObserver,
                      Ci.flockIRDFObserver, Ci.nsINavHistoryObserver,
                      Ci.nsITimerCallback, Ci.flockILuceneIndexListener,
                      Ci.flockIMigratable, Ci.nsIDOMEventListener];
    aCount.value = interfaces.length;
    return interfaces;
  },

  // nsIClassInfo
  getHelperForLanguage: function INDEXER_getHelperForLanguage(aLanguage) {
    return null;
  },

  // nsIClassInfo
  contractID: CONTRACT_ID,

  // nsIClassInfo
  classDescription: CLASS_NAME,

  // nsIClassInfo
  classID: CLASS_ID,

  // nsIClassInfo
  implementationLanguage: Ci.nsIProgrammingLanguage.JAVASCRIPT,

  // nsIClassInfo
  flags: Ci.nsIClassInfo.SINGLETON,
  
  // nsISupports
  QueryInterface: function INDEXER_QueryInterface(aIID) {
    if (!aIID.equals(Ci.nsISupports) &&
        !aIID.equals(Ci.flockIIndexer) &&
        !aIID.equals(Ci.nsIClassInfo) &&
        !aIID.equals(Ci.nsIObserver) &&
        !aIID.equals(Ci.flockIRDFObserver) &&
        !aIID.equals(Ci.nsINavHistoryObserver) &&
        !aIID.equals(Ci.nsITimerCallback) &&
        !aIID.equals(Ci.flockILuceneIndexListener) &&
        !aIID.equals(Ci.flockIMigratable) &&
        !aIID.equals(Ci.nsIDOMEventListener))
      throw Cr.NS_ERROR_NO_INTERFACE;
    return this;
  }

};

/******************************************************************************
 * XPCOM Functions for construction and registration
 ******************************************************************************/
var Module = {
  _firstTime: true,
  registerSelf: function(aCompMgr, aFileSpec, aLocation, aType) {
    if (this._firstTime) {
      this._firstTime = false;
      throw Cr.NS_ERROR_FACTORY_REGISTER_AGAIN;
    }
    aCompMgr = aCompMgr.QueryInterface(Ci.nsIComponentRegistrar);
    aCompMgr.registerFactoryLocation(CLASS_ID, CLASS_NAME, CONTRACT_ID, aFileSpec, aLocation, aType);
    
    var catman = Cc["@mozilla.org/categorymanager;1"].getService(Ci.nsICategoryManager);
    catman.addCategoryEntry("flockMigratable", CLASS_NAME, CONTRACT_ID, true, true);
  },

  unregisterSelf: function(aCompMgr, aLocation, aType) {
    aCompMgr = aCompMgr.QueryInterface(Ci.nsIComponentRegistrar);
    aCompMgr.unregisterFactoryLocation(CLASS_ID, aLocation);        
  },
  
  getClassObject: function(aCompMgr, aCID, aIID) {
    if (!aIID.equals(Ci.nsIFactory))
      throw Cr.NS_ERROR_NOT_IMPLEMENTED;
    if (aCID.equals(CLASS_ID))
      return Factory;
    throw Cr.NS_ERROR_NO_INTERFACE;
  },

  canUnload: function(aCompMgr) { return true; }
};

var Factory = {
  createInstance: function(aOuter, aIID)
  {
    if (aOuter != null)
      throw Cr.NS_ERROR_NO_AGGREGATION;
    return (new flockIndexer()).QueryInterface(aIID);
  }
};

function NSGetModule(aCompMgr, aFileSpec) { return Module; }
