/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */

this.EXPORTED_SYMBOLS = ['Gloda'];

var Cc = Components.classes;
var Ci = Components.interfaces;
var Cr = Components.results;
var Cu = Components.utils;

Cu.import("resource:///modules/gloda/log4moz.js");

Cu.import("resource:///modules/gloda/datastore.js");
Cu.import("resource:///modules/gloda/datamodel.js");
Cu.import("resource:///modules/gloda/databind.js");
Cu.import("resource:///modules/gloda/collection.js");
Cu.import("resource:///modules/gloda/connotent.js");
Cu.import("resource:///modules/gloda/query.js");
Cu.import("resource:///modules/gloda/utils.js");

Cu.import("resource:///modules/iteratorUtils.jsm");
Cu.import("resource:///modules/IOUtils.js");
Cu.import("resource:///modules/mailServices.js");
Cu.import("resource://gre/modules/Services.jsm");

/**
 * @see |Gloda.BadItemContentsError|
 */
function BadItemContentsError(aMessage) {
  this.message = aMessage;
}
BadItemContentsError.prototype = {
  toString: function BadItemContentsError_toString() {
    return this.message;
  }
};


/**
 * Provides the user-visible (and extension visible) global database
 *  functionality.  There is currently a dependency/ordering
 *  problem in that the concept of 'gloda' also includes some logic that is
 *  contributed by built-in extensions, if you will.  Those built-in extensions
 *  (fundattr.js, explattr.js) also import this file.  To avoid a circular
 *  dependency, those built-in extensions are loaded by everybody.js.  The
 *  simplest/best solution is probably to move everybody.js to be gloda.js and
 *  have it re-export only 'Gloda'.  gloda.js (this file) can then move to be
 *  gloda_int.js (or whatever our eventual naming scheme is), which built-in
 *  extensions can explicitly rely upon.
 *
 * === Concepts
 *
 * == Nouns
 *
 * Inspired by reasonable uses of triple-stores, I have tried to leverage
 *  existing model and terminology rather than rolling out own for everything.
 *  The idea with triple-stores is that you have a subject, a predicate, and an
 *  object.  For example, if we are talking about a message, that is the
 *  subject, the predicate could roughly be sent-by, and the object a person.
 *  We can generalize this idea to say that the subject and objects are nouns.
 * Since we want to be more flexible than only dealing with messages, we
 *  therefore introduce the concept of nouns as an organizing principle.
 *
 * == Attributes
 *
 * Our attributes definitions are basically our predicates.  When we define
 *  an attribute, it's a label with a bunch of meta-data.  Our attribute
 *  instances are basically a 'triple' in a triple-store.  The attributes
 *  are stored in database rows that imply a specific noun-type (ex: the
 *  messageAttributes table), with an ID identifying the message which is our
 *  subject, an attribute ID which identifies the attribute definition in use
 *  (and therefore the predicate), plus an object ID (given context aka the
 *  noun type by the attribute's meta-data) which identifies the 'object'.
 *
 * == But...
 *
 * Things aren't entirely as clear as they could be right now, terminology/
 *  concept/implementation-wise.  Some work is probably still in order.
 *
 * === Implementation
 *
 * == Nouns
 *
 * So, we go and define the nouns that are roughly the classes in our data
 *  model.  Every 'class' we define in datamodel.js is a noun that gets defined
 *  here in the Gloda core.  We provide sufficient meta-data about the noun to
 *  serialize/deserialize its representation from our database representation.
 *  Nouns do not have to be defined in this class, but can also be contributed
 *  by external code.
 * We have a concept of 'first class' nouns versus non-first class nouns.  The
 *  distinction is meant to be whether we can store meta-information about those
 *  nouns using attributes.  Right now, only message are real first-class nouns,
 *  but we want to expand that to include contacts and eventually events and
 *  tasks as lightning-integration occurs.  In practice, we are stretching the
 *  definition of first-class nouns slightly to include things we can't store
 *  meta-data about, but want to be able to query about.  We do want to resolve
 *  this.
 *
 * == Attributes
 *
 * Attributes are defined by "attribute providers" who are responsible for
 *  taking an instance of a first-class noun (for which they are registered)
 *  plus perhaps some other meta-data, and returning a list of attributes
 *  extracted from that noun.  For now, this means messages.  Attribute
 *  providers may create new data records as a side-effect of the indexing
 *  process, although we have not yet fully dealt with the problem of deleting
 *  these records should they become orphaned in the database due to the
 *  purging of a message and its attributes.
 * All of the 'core' gloda attributes are provided by the fundattr.js and
 *  explattr.js providers.
 *
 * === (Notable) Future Work
 *
 * == Attributes
 *
 * Attribute mechanisms currently lack any support for 'overriding' attributes
 *  provided by other attribute providers.  For example, the fundattr provider
 *  tells us who a message is 'from' based on the e-mail address present.
 *  However, other plugins may actually know better.  For example, the bugzilla
 *  daemon e-mails based on bug activity although the daemon gets the credit
 *  as the official sender.  A bugzilla plugin can easily extract the actual
 *  person/e-mail addressed who did something on the bug to cause the
 *  notification to be sent.  In practice, we would like that person to be
 *  the 'sender' of the bugmail.  But we can't really do that right, yet.
 *
 * @namespace
 */
var Gloda = {
  /**
   * Initialize logging, the datastore (SQLite database), the core nouns and
   *  attributes, and the contact and identities that belong to the presumed
   *  current user (based on accounts).
   *
   * Additional nouns and the core attribute providers are initialized by the
   *  everybody.js module which ensures all of those dependencies are loaded
   *  (and initialized).
   */
  _init: function gloda_ns_init() {
    this._initLogging();
    GlodaDatastore._init(this._nounIDToDef);
    this._initAttributes();
    this._initMyIdentities();
  },

  _log: null,
  /**
   * Initialize logging; the error console window gets Warning/Error, and stdout
   *  (via dump) gets everything.
   */
  _initLogging: function gloda_ns_initLogging() {
    let formatter = new Log4Moz.BasicFormatter();
    Log4Moz.repository.rootLogger.level = Log4Moz.Level.Debug;

    let enableConsoleLogging = false;
    let enableDumpLogging = false;
    // should we assume there is someone else consuming our log4moz stream?
    let enableUpstreamLogging = false;
    let considerNetLogging = false;

    let glodaLog = Log4Moz.repository.getLogger("gloda");
    glodaLog.level = Log4Moz.Level.Warn;

    try {
      // figure out if event-driven indexing should be enabled...
      let branch = Services.prefs.getBranch("mailnews.database.global.logging.");
      enableConsoleLogging = branch.getBoolPref("console");
      enableDumpLogging = branch.getBoolPref("dump");
      enableUpstreamLogging = branch.getBoolPref("upstream");
      considerNetLogging = branch.getBoolPref("net");
    } catch (ex) {}

    if (enableConsoleLogging) {
      let capp = new Log4Moz.ConsoleAppender(formatter);
      capp.level = Log4Moz.Level.Warn;
      glodaLog.addAppender(capp);
    }

    if (enableDumpLogging) {
      let dapp = new Log4Moz.DumpAppender(formatter);
      dapp.level = Log4Moz.Level.All;
      glodaLog.level = Log4Moz.Level.All;
      glodaLog.addAppender(dapp);
    }

    if (enableUpstreamLogging) {
      glodaLog.level = Log4Moz.Level.All;
    }

    if (considerNetLogging) {
      let file = Services.dirsvc.get("TmpD", Ci.nsIFile);
      file.append("chainsaw.ptr");
      if (file.exists()) {
        let data = IOUtils.loadFileToString(file);
        data = data.trim();
        let [host, port] = data.split(":");
        let xf = new Log4Moz.XMLFormatter();
        let sapp = new Log4Moz.SocketAppender(host, Number(port), xf);
        sapp.level = Log4Moz.Level.All;
        glodaLog.level = Log4Moz.Level.All;
        glodaLog.addAppender(sapp);
      }
    }

    this._log = Log4Moz.repository.getLogger("gloda.NS");
    this._log.info("Logging Initialized");
  },

  /**
   * The indexer is idle.
   */
  kIndexerIdle: 0,
  /**
   * The indexer is doing something.  We used to have other specific states, but
   *  they have been rendered irrelevant and wiped from existence.
   */
  kIndexerIndexing: 1,

  /**
   * Synchronous activities performed that can be thought of as one processing
   *  token.  Potentially yield the event-loop and re-schedule for later based
   *  on how long we've actually taken/etc.  The goal here is that code that
   *  is doing stuff synchronously yields with kWorkSync periodically to make
   *  sure that it doesn't dominate the event-loop.  Unless the processing
   *  in question is particularly intensive, it should be reasonable to apply
   *  some decimation factor (ex: 32 or 64) with the general goal of yielding
   *  every 3-10 milliseconds.
   */
  kWorkSync: 0,
  /**
   * Asynchronous activity performed, you need to relinquish flow control and
   *  trust us to call callbackDriver later.
   */
  kWorkAsync: 1,
  /**
   * We are all done with our task, close us and figure out something else to do.
   */
  kWorkDone: 2,
  /**
   * We are not done with our task, but we think it's a good idea to take a
   *  breather because we believe we have tied up the event loop for a
   *  non-trivial amount of time.  So please re-schedule us in the future.
   *
   * This is currently only used internally by the indexer's batching logic;
   *  minor changes may be required if used by actual indexers.
   */
  kWorkPause: 3,
  /**
   * We are done with our task, and have a result that we are returning.  This
   *  should only be used by your callback handler's doneWithResult method.
   *  Ex: you are passed aCallbackHandle, and you do
   *  "yield aCallbackHandle.doneWithResult(myResult);".
   */
  kWorkDoneWithResult: 4,

  /**
   * Callers should access the unique ID for the GlodaDatastore
   * with this getter. If the GlodaDatastore has not been
   * initialized, this value is null.
   *
   * @return a UUID as a string, ex: "c4dd0159-9287-480f-a648-a4613e147fdb"
   */
  get datastoreID() {
    return GlodaDatastore._datastoreID;
  },

  /**
   * Lookup a gloda message from an nsIMsgDBHdr, with the result returned as a
   *  collection.  Keep in mind that the message may not be indexed, so you
   *  may end up with an empty collection.  (Also keep in mind that this query
   *  is asynchronous, so you will want your action-taking logic to be found
   *  in your listener's onQueryCompleted method; the result will not be in
   *  the collection when this method returns.)
   *
   * @param aMsgHdr The header of the message you want the gloda message for.
   * @param aListener The listener that should be registered with the collection
   * @param aData The (optional) value to set as the data attribute on the
   *     collection.
   *
   * @return The collection that will receive the results.
   *
   * @testpoint gloda.ns.getMessageCollectionForHeader()
   */
  getMessageCollectionForHeader: function gloda_ns_getMessageForHeader(aMsgHdr,
      aListener, aData) {
    let query = Gloda.newQuery(Gloda.NOUN_MESSAGE);
    query.folder(aMsgHdr.folder).messageKey(aMsgHdr.messageKey);
    return query.getCollection(aListener, aData);
  },

  /**
   * Given a list of message headers, return a collection containing the gloda
   *  messages that correspond to those headers.  Keep in mind that gloda may
   *  not have indexed all the messages, so the returned collection may not have
   *  a message for each header you provide. (Also keep in mind that this query
   *  is asynchronous, so you will want your action-taking logic to be found
   *  in your listener's onQueryCompleted method; no results will be present in
   *  the collection when this method returns.)
   *
   * @param aHeaders A javascript Array or and XPCOM list that fixIterator can
   *     can handle.
   * @param aListener The listener that should be registered with the collection
   * @param aData The (optional) value to set as the data attribute on the
   *     collection.
   *
   * @return The collection that will receive the results.
   *
   * @testpoint gloda.ns.getMessageCollectionForHeaders()
   */
  getMessageCollectionForHeaders: function gloda_ns_getMessagesForHeaders(
      aHeaders, aListener, aData) {
    // group the headers by the folder they are found in
    let headersByFolder = {};
    let iter;
    for (let header in fixIterator(aHeaders)) {
      let folderURI = header.folder.URI;
      let headersForFolder = headersByFolder[folderURI];
      if (headersForFolder === undefined)
        headersByFolder[folderURI] = [header];
      else
        headersForFolder.push(header);
    }

    let query = Gloda.newQuery(Gloda.NOUN_MESSAGE);
    let clause;
    // build a query, using a separate union clause for each folder.
    for (let folderURI in headersByFolder) {
      let headersForFolder = headersByFolder[folderURI];
      let folder = this.getFolderForFolder(headersForFolder[0].folder);
      // if this is the first or clause, just use the query itself
      if (!clause)
        clause = query;
      else // create a new query clause via the 'or' command
        clause = query.or();

      clause.folder(folder);
      let messageKeys = headersForFolder.map(hdr => hdr.messageKey);
      clause.messageKey.apply(clause, messageKeys);
    }

    return query.getCollection(aListener, aData);
  },

  /**
   * @testpoint gloda.ns.getMessageContent
   */
  getMessageContent: function gloda_ns_getMessageContent(aGlodaMessage, aMimeMsg) {
    return mimeMsgToContentAndMeta(aMimeMsg, aGlodaMessage.folderMessage.folder)[0];
  },

  getFolderForFolder: function gloda_ns_getFolderForFolder(aMsgFolder) {
    return GlodaDatastore._mapFolder(aMsgFolder);
  },

  /**
   * Takes one or more strings containing lists of comma-delimited e-mail
   *  addresses with optional display names, and returns a list of sub-lists of
   *  identities, where each sub-list corresponds to each of the strings passed
   *  as arguments.  These identities are loaded from the database if they
   *  already exist, or created if they do not yet exist.
   * If the identities need to be created, they will also result in the
   *  creation of a gloda contact.  If a display name was provided with the
   *  e-mail address, it will become the name of the gloda contact.  If a
   *  display name was not provided, the e-mail address will also serve as the
   *  contact name.
   * This method uses the indexer's callback handle mechanism, and does not
   *  obey traditional return semantics.
   *
   * We normalize all e-mail addresses to be lowercase as a normative measure.
   *
   * @param aCallbackHandle The GlodaIndexer callback handle (or equivalent)
   *   that you are operating under.
   * @param ... One or more strings.  Each string can contain zero or more
   *   e-mail addresses with display name.  If more than one address is given,
   *   they should be comma-delimited.  For example
   *   '"Bob Smith" <bob@example.com>' is an address with display name.  Mime
   *   header decoding is performed, but is ignorant of any folder-level
   *   character set overrides.
   * @returns via the callback handle mechanism, a list containing one sub-list
   *   for each string argument passed.  Each sub-list containts zero or more
   *   GlodaIdentity instances corresponding to the addresses provided.
   */
  getOrCreateMailIdentities:
      function* gloda_ns_getOrCreateMailIdentities(aCallbackHandle) {
    let addresses = {};
    let resultLists = [];

    // parse the strings
    for (let iArg = 1; iArg < arguments.length; iArg++) {
      let aMailAddresses = arguments[iArg];
      let parsed = GlodaUtils.parseMailAddresses(aMailAddresses);

      let resultList = [];
      resultLists.push(resultList);

      let identities = [];
      for (let iAddress = 0; iAddress < parsed.count; iAddress++) {
        let address = parsed.addresses[iAddress].toLowerCase();
        if (address in addresses)
          addresses[address].push(resultList);
        else
          addresses[address] = [parsed.names[iAddress], resultList];
      }
    }

    let addressList = Object.keys(addresses);
    if (addressList.length == 0) {
      yield aCallbackHandle.doneWithResult(resultLists);
      // we should be stopped before we reach this point, but safety first.
      return;
    }

    let query = this.newQuery(this.NOUN_IDENTITY);
    query.kind("email");
    query.value.apply(query, addressList);
    let collection = query.getCollection(aCallbackHandle);
    yield this.kWorkAsync;

    // put the identities in the appropriate result lists
    for (let identity of collection.items) {
      let nameAndResultLists = addresses[identity.value];
      this._log.debug(" found identity for '" + nameAndResultLists[0] + "' (" +
                      identity.value + ")");
      // index 0 is the name, skip it
      for (let iResList = 1; iResList < nameAndResultLists.length; iResList++) {
        nameAndResultLists[iResList].push(identity);
      }
      delete addresses[identity.value];
    }

    // create the identities that did not exist yet
    for (let address in addresses) {
      let nameAndResultLists = addresses[address];
      let name = nameAndResultLists[0];

      this._log.debug(" creating contact for '" + name + "' (" + address + ")");

      // try and find an existing address book contact.
      let card = GlodaUtils.getCardForEmail(address);
      // XXX when we have the address book GUID stuff, we need to use that to
      //  find existing contacts... (this will introduce a new query phase
      //  where we batch all the GUIDs for an async query)
      // XXX when the address book supports multiple e-mail addresses, we
      //  should also just create identities for any that don't yet exist

      // if there is no name, just use the e-mail (the ab indexer actually
      //  processes the card's displayName for synchronization, so we don't
      //  need to do that.)
      if (!name)
        name = address;

      let contact = GlodaDatastore.createContact(null, null, name, 0, 0);

      // we must create the identity.  use a blank description because there's
      //  nothing to differentiate it from other identities, as this contact
      //  only has one initially (us).
      // XXX when we have multiple e-mails and there is a meaning associated
      //  with each e-mail, try and use that to populate the description.
      // XXX we are creating the identity here before we insert the contact.
      //  conceptually it is good for us to be creating the identity before
      //  exposing it to the address-book indexer, but we could get our id's
      //  in a bad way from not deferring the identity insertion until after
      //  the contact insertion.
      let identity = GlodaDatastore.createIdentity(contact.id, contact,
        "email", address, /* description */ "", /* relay? */ false);
      contact._identities = [identity];

      // give the address book indexer a chance if we have a card.
      // (it will fix-up the name based on the card as appropriate)
      if (card)
        yield aCallbackHandle.pushAndGo(
          Gloda.grokNounItem(contact, {card: card}, true, true,
                             aCallbackHandle));
      else // grokNounItem will issue the insert for us...
        GlodaDatastore.insertContact(contact);

      for (let iResList = 1; iResList < nameAndResultLists.length; iResList++) {
        nameAndResultLists[iResList].push(identity);
      }
    }

    yield aCallbackHandle.doneWithResult(resultLists);
  },

  /**
   * Dictionary of the user's known identities; key is the identity id, value
   *  is the actual identity.  This is populated by _initMyIdentities based on
   *  the accounts defined.
   */
  myIdentities: {},
  /**
   * The contact corresponding to the current user.  We are assuming that only
   *  a single user/human being uses the current profile.  This is known to be
   *  a flawed assumption, but is the best first approximation available.
   *
   * @TODO attempt to deal with multile people using the same profile
   */
  myContact: null,
  /**
   * Populate myIdentities with all of our identities.  Currently we do this
   *  by assuming that there is one human/user per profile, and that all of the
   *  accounts defined in the profile belong to them.  The single contact is
   *  stored on myContact.
   *
   * @TODO deal with account addition/modification/removal
   * @TODO attempt to deal with multiple people using the same profile
   */
  _initMyIdentities: function gloda_ns_initMyIdentities() {
    let myContact = null;
    let myIdentities = {};
    let myEmailAddresses = {}; // process each email at most once; stored here

    let fullName, fallbackName;
    let existingIdentities = [];
    let identitiesToCreate = [];

    let numIdentities = MailServices.accounts.allIdentities.length;

    // nothing to do if there are no accounts/identities.
    if (!numIdentities)
      return;

    for (let iIdentity = 0; iIdentity < numIdentities; iIdentity++) {
      let msgIdentity =
        MailServices.accounts.allIdentities.queryElementAt(iIdentity,
                                                           Ci.nsIMsgIdentity);

      if (!fullName)
        fullName = msgIdentity.fullName;
      if (!fallbackName)
        fallbackName = msgIdentity.email;

      let emailAddress = msgIdentity.email;
      let replyTo = msgIdentity.replyTo;

      // find the identities if they exist, flag to create them if they don't
      if (emailAddress) {
        let parsed = GlodaUtils.parseMailAddresses(emailAddress);
        if (!(parsed.addresses[0] in myEmailAddresses)) {
          let identity = GlodaDatastore.getIdentity("email",
                                                    parsed.addresses[0]);
          if (identity)
            existingIdentities.push(identity);
          else
            identitiesToCreate.push(parsed.addresses[0]);
          myEmailAddresses[parsed.addresses[0]] = true;
        }
      }
      if (replyTo) {
        let parsed = GlodaUtils.parseMailAddresses(replyTo);
        if (!(parsed.addresses[0] in myEmailAddresses)) {
          let identity = GlodaDatastore.getIdentity("email",
                                                    parsed.addresses[0]);
          if (identity)
            existingIdentities.push(identity);
          else
            identitiesToCreate.push(parsed.addresses[0]);
          myEmailAddresses[parsed.addresses[0]] = true;
        }
      }
    }

    // we need to establish the identity.contact portions of the relationship
    for (let identity of existingIdentities) {
      identity._contact = GlodaDatastore.getContactByID(identity.contactID);
    }

    if (existingIdentities.length) {
      // just use the first guy's contact
      myContact = existingIdentities[0].contact;
    }
    else {
      // create a new contact
      myContact = GlodaDatastore.createContact(null, null,
                                               fullName || fallbackName,
                                               0, 0);
      GlodaDatastore.insertContact(myContact);
    }

    if (identitiesToCreate.length) {
      for (let iIdentity = 0; iIdentity < identitiesToCreate.length;
          iIdentity++) {
        let emailAddress = identitiesToCreate[iIdentity];
        // XXX this won't always be of type "email" as we add new account types
        // XXX the blank string could be trying to differentiate; we do have
        //  enough info to do it.
        let identity = GlodaDatastore.createIdentity(myContact.id, myContact,
                                                     "email",
                                                     emailAddress,
                                                     "", false);
        existingIdentities.push(identity);
      }
    }

    for (let iIdentity = 0; iIdentity < existingIdentities.length;
        iIdentity++) {
      let identity = existingIdentities[iIdentity];
      myIdentities[identity.id] = identity;
    }

    this.myContact = myContact;
    this.myIdentities = myIdentities;
    myContact._identities = Object.keys(myIdentities).
      map(id => myIdentities[id]);

    // we need contacts to make these objects reachable via the collection
    //  manager.
    this._myContactCollection = this.explicitCollection(this.NOUN_CONTACT,
                                                        [this.myContact]);
    this._myIdentitiesCollection =
      this.explicitCollection(this.NOUN_IDENTITY, this.myContact._identities);
  },

  /**
   * An attribute that is a defining characteristic of the subject.
   */
  kAttrFundamental: 0,
  /**
   * An attribute that is an optimization derived from two or more fundamental
   *  attributes and exists solely to improve database query performance.
   */
  kAttrOptimization: 1,
  /**
   * An attribute that is derived from the content of the subject.  For example,
   *  a message that references a bugzilla bug could have a "derived" attribute
   *  that captures the bugzilla reference.  This is not
   */
  kAttrDerived: 2,
  /**
   * An attribute that is the result of an explicit and intentional user action
   *  upon the subject.  For example, a tag placed on a message by a user (or
   *  at the user's request by a filter) is explicit.
   */
  kAttrExplicit: 3,
  /**
   * An attribute that is indirectly the result of a user's behaviour.  For
   *  example, if a user consults a message multiple times, we may conclude that
   *  the user finds the message interesting.  It is "implied", if you will,
   *  that the message is interesting.
   */
  kAttrImplicit: 4,

  /**
   * This attribute is not 'special'; it is stored as a (thing id, attribute id,
   *  attribute id) tuple in the database rather than on thing's row or on
   *  thing's fulltext row.  (Where "thing" could be a message or any other
   *  first class noun.)
   */
  kSpecialNotAtAll: GlodaDatastore.kSpecialNotAtAll,
  /**
   * This attribute is stored as a numeric column on the row for the noun.  The
   *  attribute definition should include this value as 'special' and the
   *  column name that stores the attribute as 'specialColumnName'.
   */
  kSpecialColumn: GlodaDatastore.kSpecialColumn,
  kSpecialColumnChildren: GlodaDatastore.kSpecialColumnChildren,
  kSpecialColumnParent: GlodaDatastore.kSpecialColumnParent,
  /**
   * This attribute is stored as a string column on the row for the noun.  It
   *  differs from kSpecialColumn in that it is a string, which once had
   *  query ramifications and one day may have them again.
   */
  kSpecialString: GlodaDatastore.kSpecialString,
  /**
   * This attribute is stored as a fulltext column on the fulltext table for
   *  the noun.  The attribute defintion should include this value as 'special'
   *  and the column name that stores the table as 'specialColumnName'.
   */
  kSpecialFulltext: GlodaDatastore.kSpecialFulltext,

  /**
   * The extensionName used for the attributes defined by core gloda plugins
   *  such as fundattr.js and explattr.js.
   */
  BUILT_IN: "built-in",

  /**
   * Special sentinel value that will cause facets to skip a noun instance
   * when an attribute has this value.
   */
  IGNORE_FACET: GlodaDatastore.IGNORE_FACET,

  /*
   * The following are explicit noun IDs.  While most extension-provided nouns
   *  will have dynamically allocated id's that are looked up by name, these
   *  id's can be relied upon to exist and be accessible via these
   *  pseudo-constants.  It's not really clear that we need these, although it
   *  does potentially simplify code to not have to look up all of their nouns
   *  at initialization time.
   */
  /**
   * Boolean values, expressed as 0/1 in the database and non-continuous for
   *  constraint purposes.  Like numbers, such nouns require their attributes
   *  to provide them with context, lacking any of their own.
   * Having this as a noun type may be a bad idea; a change of nomenclature
   *  (so that we are not claiming a boolean value is a noun, but still using
   *  it in the same way) or implementation to require each boolean noun
   *  actually be its own noun may be in order.
   */
  NOUN_BOOLEAN: 1,
  /**
   * A number, which could mean an integer or floating point values.  We treat
   *  these as continuous, meaning that queries on them can have ranged
   *  constraints expressed on them.  Lacking any inherent context, numbers
   *  depend on their attributes to parameterize them as required.
   * Same deal as with NOUN_BOOLEAN, we may need to change this up conceptually.
   */
  NOUN_NUMBER: 2,
  /**
   * A (non-fulltext) string.
   * Same deal as with NOUN_BOOLEAN, we may need to change this up conceptually.
   */
  NOUN_STRING: 3,
  /** A date, encoded as a PRTime, represented as a js Date object. */
  NOUN_DATE: 10,
  /**
   * Fulltext search support, somewhat magical.  This is only intended to be
   *  used for kSpecialFulltext attributes, and exclusively as a constraint
   *  mechanism.  The values are always represented as strings.  It is presumed
   *  that the user of this functionality knows how to generate SQLite FTS3
   *  style MATCH queries, or is okay with us just gluing them together with
   *  " OR " when used in an or-constraint case.  Gloda's query mechanism
   *  currently lacks the ability to to compile Gloda-style and-constraints
   *  into a single MATCH query, but it will turn out okay, just less
   *  efficiently than it could.
   */
  NOUN_FULLTEXT: 20,
  /**
   * Represents a MIME Type.  We currently lack any human-intelligible
   *  descriptions of mime types.
   */
  NOUN_MIME_TYPE: 40,
  /**
   * Captures a message tag as well as when the tag's presence was observed,
   *  hoping to approximate when the tag was applied.  It's a somewhat dubious
   *  attempt to not waste our opporunity to store a value along with the tag.
   *  (The tag is actually stored as an attribute parameter on the attribute
   *  definition, rather than a value in the attribute 'instance' for the
   *  message.)
   */
  NOUN_TAG: 50,
  /**
   * Doesn't actually work owing to a lack of an object to represent a folder.
   *  We do expose the folderURI and folderID of a message, but need to map that
   *  to a good abstraction.  Probably something thin around a SteelFolder or
   *  the like; we would contribute the functionality to easily move from a
   *  folder to the list of gloda messages in that folder, as well as the
   *  indexing preferences for that folder.
   * @TODO folder noun and related abstraction
   */
  NOUN_FOLDER: GlodaFolder.prototype.NOUN_ID, // 100
  /**
   * All messages belong to a conversation.  See datamodel.js for the
   *  definition of the GlodaConversation class.
   */
  NOUN_CONVERSATION: GlodaConversation.prototype.NOUN_ID, // 101
  /**
   * A one-to-one correspondence with underlying (indexed) nsIMsgDBHdr
   *  instances.  See datamodel.js for the definition of the GlodaMessage class.
   */
  NOUN_MESSAGE: GlodaMessage.prototype.NOUN_ID, // 102
  /**
   * Corresponds to a human being, who may have multiple electronic identities
   *  (a la NOUN_IDENTITY).  There is no requirement for association with an
   *  address book contact, although when the address book contact exists,
   *  we want to be associated with it.  See datamodel.js for the definition
   *  of the GlodaContact class.
   */
  NOUN_CONTACT: GlodaContact.prototype.NOUN_ID, // 103
  /**
   * A single identity of a contact, who may have one or more.  E-mail accounts,
   *  instant messaging accounts, social network site accounts, etc. are each
   *  identities.  See datamodel.js for the definition of the GlodaIdentity
   *  class.
   */
  NOUN_IDENTITY: GlodaIdentity.prototype.NOUN_ID, // 104
  /**
   * An attachment to a message. A message may have many different attachments.
   */
  NOUN_ATTACHMENT: GlodaAttachment.prototype.NOUN_ID, // 105
  /**
   * An account related to a message. A message can have only one account.
   */
  NOUN_ACCOUNT: GlodaAccount.prototype.NOUN_ID, // 106

  /**
   * Parameterized identities, for use in the from-me, to-me, cc-me optimization
   *  cases.  Not for reuse without some thought.  These nouns use the parameter
   *  to store the 'me' identity that we are talking about, and the value to
   *  store the identity of the other party.  So in both the from-me and to-me
   *  cases involving 'me' and 'foo@bar', the 'me' identity is always stored via
   *  the attribute parameter, and the 'foo@bar' identity is always stored as
   *  the attribute value.  See fundattr.js for more information on this, but
   *  you probably shouldn't be touching this unless you are fundattr.
   */
  NOUN_PARAM_IDENTITY: 200,

  /** Next Noun ID to hand out, these don't need to be persisted (for now). */
  _nextNounID: 1000,

  /**
   * Maps noun names to noun IDs.
   */
  _nounNameToNounID: {},
  /**
   * Maps noun IDs to noun definition dictionaries.  (Noun definition
   *  dictionaries provided to us at the time a noun was defined, plus some
   *  additional stuff we put in there.)
   */
  _nounIDToDef: {},

  _managedToJSON: function gloda_ns_managedToJSON(aItem) {
    return aItem.id;
  },

  /**
   * Define a noun.  Takes a dictionary with the following keys/values:
   *
   * @param aNounDef.name The name of the noun.  This is not a display name
   *     (anything being displayed needs to be localized, after all), but simply
   *     the canonical name for debugging purposes and for people to pass to
   *     lookupNoun.  The suggested convention is lower-case-dash-delimited,
   *     with names being singular (since it's a single noun we are referring
   *     to.)
   * @param aNounDef.class The 'class' to which an instance of the noun will
   *     belong (aka will pass an instanceof test).  You may also provide this
   *     as 'clazz' if the keyword makes your IDE angry.
   * @param aNounDef.allowsArbitraryAttrs Is this a 'first class noun'/can it be
   *     a subject, AKA can this noun have attributes stored on it that relate
   *     it to other things?  For example, a message is first-class; we store
   *     attributes of messages.  A date is not first-class now, nor is it
   *     likely to be; we will not store attributes about a date, although dates
   *     will be the objects of other subjects.  (For example: we might
   *     associate a date with a calendar event, but the date is an attribute of
   *     the calendar event and not vice versa.)
   * @param aNounDef.usesParameter A boolean indicating whether this noun
   *     requires use of the 'parameter' BLOB storage field on the attribute
   *     bindings in the database to persist itself.  Use of parameters should
   *     be limited to a reasonable number of values (16-32 is okay, more than
   *     that is pushing it and 256 should be considered an absolute upper
   *     bound) because of the database organization.  When false, your
   *     toParamAndValue function is expected to return null for the parameter
   *     and likewise your fromParamAndValue should expect ignore and generally
   *     ignore the argument.
   * @param aNounDef.toParamAndValue A function that takes an instantiated noun
   *     instance and returns a 2-element list of [parameter, value] where
   *     parameter may only be non-null if you passed a usesParameter of true.
   *     Parameter may be of any type (BLOB), and value must be numeric (pass
   *     0 if you don't need the value).
   *
   * @param aNounDef.isPrimitive True when the noun instance is a raw numeric
   *     value/string/boolean.  False when the instance is an object.  When
   *     false, it is assumed the attribute that serves as a unique identifier
   *     for the value is "id" unless 'idAttr' is provided.
   * @param [aNounDef.idAttr="id"] For non-primitive nouns, this is the
   *     attribute on the object that uniquely identifies it.
   *
   * @param aNounDef.schema Unsupported mechanism by which you can define a
   *     table that corresponds to this noun.  The table will be created if it
   *     does not exist.
   *     - name The table name; don't conflict with other things!
   *     - columns A list of [column name, sqlite type] tuples.  You should
   *       always include a definition like ["id", "INTEGER PRIMARY KEY"] for
   *       now (and it should be the first column name too.)  If you care about
   *       how the attributes are poked into your object (for example, you want
   *       underscores used for some of them because the attributes should be
   *       immutable), then you can include a third string that is the name of
   *       the attribute to use.
   *     - indices A dictionary of lists of column names, where the key name
   *       becomes the index name.  Ex: {foo: ["bar"]} results in an index on
   *       the column "bar" where the index is named "foo".
  */
  defineNoun: function gloda_ns_defineNoun(aNounDef, aNounID) {
    this._log.info("Defining noun: " + aNounDef.name);
    if (aNounID === undefined)
      aNounID = this._nextNounID++;
    aNounDef.id = aNounID;

    // Let people whose editors get angry about illegal attribute names use
    //  clazz instead of class.
    if (aNounDef.clazz)
      aNounDef.class = aNounDef.clazz;

    if (!("idAttr" in aNounDef))
      aNounDef.idAttr = "id";
    if (!("comparator" in aNounDef)) {
      aNounDef.comparator = function() {
        throw new Error("Noun type '" + aNounDef.name +
                        "' lacks a real comparator.");
      };
    }

    // We allow nouns to have data tables associated with them where we do all
    //  the legwork.  The schema attribute is the gateway to this magical world
    //  of functionality.  Said door is officially unsupported.
    if (aNounDef.schema) {
      if (!aNounDef.tableName) {
        if (aNounDef.schema.name)
          aNounDef.tableName = "ext_" + aNounDef.schema.name;
        else
          aNounDef.tableName = "ext_" + aNounDef.name;
      }
      // this creates the data table and binder and hooks everything up
      GlodaDatastore.createNounTable(aNounDef);

      if (!aNounDef.toParamAndValue)
        aNounDef.toParamAndValue = function (aThing) {
          if (aThing instanceof aNounDef.class)
            return [null, aThing.id];
          else // assume they're just passing the id directly
            return [null, aThing];
        };
    }

    // if it has a table, you can query on it.  seems straight-forward.
    if (aNounDef.tableName) {
      [aNounDef.queryClass, aNounDef.nullQueryClass,
       aNounDef.explicitQueryClass, aNounDef.wildcardQueryClass] =
          GlodaQueryClassFactory(aNounDef);
      aNounDef._dbMeta = {};
      aNounDef.class.prototype.NOUN_ID = aNounDef.id;
      aNounDef.class.prototype.NOUN_DEF = aNounDef;
      aNounDef.toJSON = this._managedToJSON;

      aNounDef.specialLoadAttribs = [];

      // - define the 'id' constrainer
      let idConstrainer = function() {
        let constraint = [GlodaDatastore.kConstraintIdIn, null];
        for (let iArg = 0; iArg < arguments.length; iArg++) {
          constraint.push(arguments[iArg]);
        }
        this._constraints.push(constraint);
        return this;
      };
      aNounDef.queryClass.prototype.id = idConstrainer;
    }
    if (aNounDef.cache) {
      let cacheCost = aNounDef.cacheCost || 1024;
      let cacheBudget = aNounDef.cacheBudget || 128 * 1024;
      let cacheSize = Math.floor(cacheBudget / cacheCost);
      if (cacheSize)
        GlodaCollectionManager.defineCache(aNounDef, cacheSize);
    }
    aNounDef.attribsByBoundName = {};
    aNounDef.domExposeAttribsByBoundName = {};

    aNounDef.objectNounOfAttributes = [];

    this._nounNameToNounID[aNounDef.name] = aNounID;
    this._nounIDToDef[aNounID] = aNounDef;
    aNounDef.actions = [];

    this._attrProviderOrderByNoun[aNounDef.id] = [];
    this._attrOptimizerOrderByNoun[aNounDef.id] = [];
    this._attrProvidersByNoun[aNounDef.id] = {};

    return aNounDef;
  },

  /**
   * Lookup a noun (ID) suitable for passing to defineAttribute's various
   *  noun arguments.  Throws an exception if the noun with the given name
   *  cannot be found; the assumption is that you can't live without the noun.
   */
  lookupNoun: function gloda_ns_lookupNoun(aNounName) {
    if (aNounName in this._nounNameToNounID)
      return this._nounNameToNounID[aNounName];

    throw Error("Unable to locate noun with name '" + aNounName + "', but I " +
                "do know about: " +
                Object.keys(this._nounNameToNounID).join(", "));
  },

  /**
   * Lookup a noun def given a name.
   */
  lookupNounDef: function gloda_ns_lookupNoun(aNounName) {
    return this._nounIDToDef[this.lookupNoun(aNounName)];
  },


  /**
   * Define an action on a noun.  During the prototype stage, this was conceived
   *  of as a way to expose all the constraints possible given a noun.  For
   *  example, if you have an identity or a contact, you could use this to
   *  see all the messages sent from/to a given contact.  It was likewise
   *  thought potentially usable for future expansion.  For example, you could
   *  also decide to send an e-mail to a contact when you have the contact
   *  instance available.
   * Outside of the 'expmess' checkbox-happy prototype, this functionality is
   *  not used.  As such, this functionality should be considered in flux and
   *  subject to changes.  Also, very open to specific suggestsions motivated
   *  by use cases.
   * One conceptual issue raised by this mechanism is the interaction of actions
   *  with facts like "this message is read".  We currently implement the 'fact'
   *  by defining an attribute with a 'boolean' noun type.  To deal with this,
   *  in various places we pass-in the attribute as well as the noun value.
   *  Since the relationships for booleans and integers in these cases is
   *  standard and well-defined, this works out pretty well, but suggests we
   *  need to think things through.
   *
   * @param aNounID The ID of the noun you want to define an action on.
   * @param aActionMeta The dictionary describing the noun.  The dictionary
   *     should have the following fields:
   * - actionType: a string indicating the type of action.  Currently, only
   *   "filter" is a legal value.
   * - actionTarget: the noun ID of the noun type on which this action is
   *   applicable.  For example,
   *
   * The following should be present for actionType=="filter";
   * - shortName: The name that should be used to display this constraint.  For
   *   example, a checkbox-heavy UI might display a checkbox for each constraint
   *   using shortName as the label.
   * - makeConstraint: A function that takes the attribute that is the source
   *   of the noun and the noun instance as arguments, and returns APV-style
   *   constraints.  Since the APV-style query mechanism is now deprecated,
   *   this signature is deprecated.  Probably the way to update this would be
   *   to pass in the query instance that constraints should be contributed to.
   */
  defineNounAction: function gloda_ns_defineNounAction(aNounID, aActionMeta) {
    let nounDef = this._nounIDToDef[aNounID];
    nounDef.actions.push(aActionMeta);
  },

  /**
   * Retrieve all of the actions (as defined using defineNounAction) for the
   *  given noun type (via noun ID) with the given action type (ex: filter).
   */
  getNounActions: function gloda_ns_getNounActions(aNounID, aActionType) {
    let nounDef = this._nounIDToDef[aNounID];
    if (!nounDef)
      return [];
    return nounDef.actions.
      filter(action => !aActionType || (action.actionType == aActionType));
  },

  /** Attribute providers in the sequence to process them. */
  _attrProviderOrderByNoun: {},
  /** Attribute providers that provide optimizers, in the sequence to proc. */
  _attrOptimizerOrderByNoun: {},
  /** Maps attribute providers to the list of attributes they provide */
  _attrProviders: {},
  /**
   * Maps nouns to their attribute providers to a list of the attributes they
   *  provide for the noun.
   */
  _attrProvidersByNoun: {},

  /**
   * Define the core nouns (that are not defined elsewhere) and a few noun
   *  actions.  Core nouns could be defined in other files, assuming dependency
   *  issues are resolved via the everybody.js mechanism or something else.
   *  Right now, noun_tag defines the tag noun.  If we broke more of these out,
   *  we would probably want to move the 'class' code from datamodel.js, the
   *  SQL table def and helper code from datastore.js (and this code) to their
   *  own noun_*.js files.  There are some trade-offs to be made, and I think
   *  we can deal with those once we start to integrate lightning/calendar and
   *  our noun space gets large and more heterogeneous.
   */
  _initAttributes: function gloda_ns_initAttributes() {
    this.defineNoun({
      name: "bool",
      clazz: Boolean, allowsArbitraryAttrs: false,
      isPrimitive: true,
      // favor true before false
      comparator: function gloda_bool_comparator(a, b) {
        if (a == null) {
          if (b == null)
            return 0;
          else
            return 1;
        }
        else if (b == null) {
          return -1;
        }
        return b - a;
      },
      toParamAndValue: function(aBool) {
        return [null, aBool ? 1 : 0];
      }}, this.NOUN_BOOLEAN);
    this.defineNoun({
      name: "number",
      clazz: Number, allowsArbitraryAttrs: false, continuous: true,
      isPrimitive: true,
      comparator: function gloda_number_comparator(a, b) {
        if (a == null) {
          if (b == null)
            return 0;
          else
            return 1;
        }
        else if (b == null) {
          return -1;
        }
        return a - b;
      },
      toParamAndValue: function(aNum) {
        return [null, aNum];
      }}, this.NOUN_NUMBER);
    this.defineNoun({
      name: "string",
      clazz: String, allowsArbitraryAttrs: false,
      isPrimitive: true,
      comparator: function gloda_string_comparator(a, b) {
        if (a == null) {
          if (b == null)
            return 0;
          else
            return 1;
        }
        else if (b == null) {
          return -1;
        }
        return a.localeCompare(b);
      },
      toParamAndValue: function(aString) {
        return [null, aString];
      }}, this.NOUN_STRING);
    this.defineNoun({
      name: "date",
      clazz: Date, allowsArbitraryAttrs: false, continuous: true,
      isPrimitive: true,
      comparator: function gloda_data_comparator(a, b) {
        if (a == null) {
          if (b == null)
            return 0;
          else
            return 1;
        }
        else if (b == null) {
          return -1;
        }
        return a - b;
      },
      toParamAndValue: function(aDate) {
        return [null, aDate.valueOf() * 1000];
      }}, this.NOUN_DATE);
    this.defineNoun({
      name: "fulltext",
      clazz: String, allowsArbitraryAttrs: false, continuous: false,
      isPrimitive: true,
      comparator: function gloda_fulltext_comparator(a, b) {
        throw new Error("Fulltext nouns are not comparable!");
      },
      // as noted on NOUN_FULLTEXT, we just pass the string around.  it never
      //  hits the database, so it's okay.
      toParamAndValue: function(aString) {
        return [null, aString];
      }}, this.NOUN_FULLTEXT);

    this.defineNoun({
      name: "folder",
      clazz: GlodaFolder,
      allowsArbitraryAttrs: false,
      isPrimitive: false,
      queryHelpers: {
        /**
         * Query for accounts based on the account associated with folders.  We
         *  walk all of the folders associated with an account and put them in
         *  the list of folders that match if gloda would index them.  This is
         *  unsuitable for producing a persistable constraint since it does not
         *  adapt for added/deleted folders.  However, it is sufficient for
         *  faceting.  Also, we don't persist constraints yet.
         *
         * @TODO The long-term solution is to move towards using arithmetic
         *     encoding on folder-id's like we use for MIME types and friends.
         */
        Account: function(aAttrDef, aArguments) {
          let folderValues = [];
          let seenRootFolders = {};
          for (let iArg = 0; iArg < aArguments.length; iArg++) {
            let givenFolder = aArguments[iArg];
            let givenMsgFolder = givenFolder.getXPCOMFolder(
                                   givenFolder.kActivityFolderOnlyNoData);
            let rootFolder = givenMsgFolder.rootFolder;

            // skip processing this folder if we have already processed its
            //  root folder.
            if (rootFolder.URI in seenRootFolders)
              continue;
            seenRootFolders[rootFolder.URI] = true;

            let allFolders = rootFolder.descendants;
            for (let folder in fixIterator(allFolders, Ci.nsIMsgFolder)) {
              let folderFlags = folder.flags;

              // Ignore virtual folders, non-mail folders.
              // XXX this is derived from GlodaIndexer's shouldIndexFolder.
              //  This should probably just use centralized code or the like.
              if (!(folderFlags & Ci.nsMsgFolderFlags.Mail) ||
                  (folderFlags & Ci.nsMsgFolderFlags.Virtual))
                continue;
              // we only index local or IMAP folders
              if (!(folder instanceof Ci.nsIMsgLocalMailFolder) &&
                !(folder instanceof Ci.nsIMsgImapMailFolder))
                continue;

              let glodaFolder = Gloda.getFolderForFolder(folder);
              folderValues.push(glodaFolder);
            }
          }
          return this._inConstraintHelper(aAttrDef, folderValues);
        }
      },
      comparator: function gloda_folder_comparator(a, b) {
        if (a == null) {
          if (b == null)
            return 0;
          else
            return 1;
        }
        else if (b == null) {
          return -1;
        }
        return a.name.localeCompare(b.name);
      },
      toParamAndValue: function(aFolderOrGlodaFolder) {
        if (aFolderOrGlodaFolder instanceof GlodaFolder)
          return [null, aFolderOrGlodaFolder.id];
        else
          return [null, GlodaDatastore._mapFolder(aFolderOrGlodaFolder).id];
      }}, this.NOUN_FOLDER);
    this.defineNoun({
      name: "account",
      clazz: GlodaAccount,
      allowsArbitraryAttrs: false,
      isPrimitive: false,
      equals: function(a, b) {
        if (a && !b || !a && b)
          return false;
        if (!a && !b)
          return true;
        return a.id == b.id;
      },
      comparator: function gloda_account_comparator(a, b) {
        if (a == null) {
          if (b == null)
            return 0;
          else
            return 1;
        }
        else if (b == null) {
          return -1;
        }
        return a.name.localeCompare(b.name);
      }}, this.NOUN_ACCOUNT);
    this.defineNoun({
      name: "conversation",
      clazz: GlodaConversation,
      allowsArbitraryAttrs: false,
      isPrimitive: false,
      cache: true, cacheCost: 512,
      tableName: "conversations",
      attrTableName: "messageAttributes", attrIDColumnName: "conversationID",
      datastore: GlodaDatastore,
      objFromRow: GlodaDatastore._conversationFromRow,
      comparator: function gloda_conversation_comparator(a, b) {
        if (a == null) {
          if (b == null)
            return 0;
          else
            return 1;
        }
        else if (b == null) {
          return -1;
        }
        return a.subject.localeCompare(b.subject);
      },
      toParamAndValue: function(aConversation) {
        if (aConversation instanceof GlodaConversation)
          return [null, aConversation.id];
        else // assume they're just passing the id directly
          return [null, aConversation];
      }}, this.NOUN_CONVERSATION);
    this.defineNoun({
      name: "message",
      clazz: GlodaMessage,
      allowsArbitraryAttrs: true,
      isPrimitive: false,
      cache: true, cacheCost: 2048,
      tableName: "messages",
      // we will always have a fulltext row, even for messages where we don't
      //  have the body available.  this is because we want the subject indexed.
      dbQueryJoinMagic:
        " INNER JOIN messagesText ON messages.id = messagesText.rowid",
      attrTableName: "messageAttributes", attrIDColumnName: "messageID",
      datastore: GlodaDatastore, objFromRow: GlodaDatastore._messageFromRow,
      dbAttribAdjuster: GlodaDatastore.adjustMessageAttributes,
      dbQueryValidityConstraintSuffix:
        " AND +deleted = 0 AND +folderID IS NOT NULL AND +messageKey IS NOT NULL",
      // This is what's used when we have no validity constraints, i.e. we allow
      // for ghost messages, which do not have a row in the messagesText table.
      dbQueryJoinMagicWithNoValidityConstraints:
        " LEFT JOIN messagesText ON messages.id = messagesText.rowid",
      objInsert: GlodaDatastore.insertMessage,
      objUpdate: GlodaDatastore.updateMessage,
      toParamAndValue: function(aMessage) {
        if (aMessage instanceof GlodaMessage)
          return [null, aMessage.id];
        else // assume they're just passing the id directly
          return [null, aMessage];
      }}, this.NOUN_MESSAGE);
    this.defineNoun({
      name: "contact",
      clazz: GlodaContact,
      allowsArbitraryAttrs: true,
      isPrimitive: false,
      cache: true, cacheCost: 128,
      tableName: "contacts",
      attrTableName: "contactAttributes", attrIDColumnName: "contactID",
      datastore: GlodaDatastore, objFromRow: GlodaDatastore._contactFromRow,
      dbAttribAdjuster: GlodaDatastore.adjustAttributes,
      objInsert: GlodaDatastore.insertContact,
      objUpdate: GlodaDatastore.updateContact,
      comparator: function gloda_contact_comparator(a, b) {
        if (a == null) {
          if (b == null)
            return 0;
          else
            return 1;
        }
        else if (b == null) {
          return -1;
        }
        return a.name.localeCompare(b.name);
      },
      toParamAndValue: function(aContact) {
        if (aContact instanceof GlodaContact)
          return [null, aContact.id];
        else // assume they're just passing the id directly
          return [null, aContact];
      }}, this.NOUN_CONTACT);
    this.defineNoun({
      name: "identity",
      clazz: GlodaIdentity,
      allowsArbitraryAttrs: false,
      isPrimitive: false,
      cache: true, cacheCost: 128,
      usesUniqueValue: true,
      tableName: "identities",
      datastore: GlodaDatastore, objFromRow: GlodaDatastore._identityFromRow,
      /**
       * Short string is the contact name, long string includes the identity
       *  value too, delimited by a colon.  Not tremendously localizable.
       */
      userVisibleString: function(aIdentity, aLong) {
        if (!aLong)
          return aIdentity.contact.name;
        if (aIdentity.contact.name == aIdentity.value)
          return aIdentity.value;
        return aIdentity.contact.name + " (" + aIdentity.value + ")";
      },
      comparator: function gloda_identity_comparator(a, b) {
        if (a == null) {
          if (b == null)
            return 0;
          else
            return 1;
        }
        else if (b == null) {
          return -1;
        }
        return a.contact.name.localeCompare(b.contact.name);
      },
      toParamAndValue: function(aIdentity) {
        if (aIdentity instanceof GlodaIdentity)
          return [null, aIdentity.id];
        else // assume they're just passing the id directly
          return [null, aIdentity];
      }}, this.NOUN_IDENTITY);
    this.defineNoun({
      name: "attachment-infos",
      clazz: GlodaAttachment,
      allowsArbitraryAttrs: false,
      isPrimitive: false,
      toJSON: function (x) {
          return [
            x._name,
            x._contentType,
            x._size,
            x._part,
            x._externalUrl,
            x._isExternal
          ]
        },
      fromJSON: function (x, aGlodaMessage) {
          let [name, contentType, size, _part, _externalUrl, isExternal] = x;
          return new GlodaAttachment(aGlodaMessage, name, contentType, size, _part, _externalUrl, isExternal);
        },
      }, this.NOUN_ATTACHMENT);

    // parameterized identity is just two identities; we store the first one
    //  (whose value set must be very constrainted, like the 'me' identities)
    //  as the parameter, the second (which does not need to be constrained)
    //  as the value.
    this.defineNoun({
      name: "parameterized-identity",
      clazz: null,
      allowsArbitraryAttrs: false,
      comparator: function gloda_fulltext_comparator(a, b) {
        if (a == null) {
          if (b == null)
            return 0;
          else
            return 1;
        }
        else if (b == null) {
          return -1;
        }
        // First sort by the first identity in the tuple
        // Since our general use-case is for the first guy to be "me", we only
        //  compare the identity value, not the name.
        let fic = a[0].value.localeCompare(b[0].value);
        if (fic)
          return fic;
        // Next compare the second identity in the tuple, but use the contact
        //  this time to be consistent with our identity comparator.
        return a[1].contact.name.localeCompare(b[1].contact.name);
      },
      computeDelta: function(aCurValues, aOldValues) {
        let oldMap = {};
        for (let tupe of aOldValues) {
          let [originIdentity, targetIdentity] = tupe;
          let targets = oldMap[originIdentity];
          if (targets === undefined)
            targets = oldMap[originIdentity] = {};
          targets[targetIdentity] = true;
        }

        let added = [], removed = [];
        for (let tupe of aCurValues) {
          let [originIdentity, targetIdentity] = tupe;
          let targets = oldMap[originIdentity];
          if ((targets === undefined) || !(targetIdentity in targets))
            added.push(tupe);
          else
            delete targets[targetIdentity];
        }

        for (let originIdentity in oldMap) {
          let targets = oldMap[originIdentity];
          for (let targetIdentity in targets) {
            removed.push([originIdentity, targetIdentity]);
          }
        }

        return [added, removed];
      },
      contributeObjDependencies: function(aJsonValues, aReferencesByNounID,
          aInverseReferencesByNounID) {
        // nothing to do with a zero-length list
        if (aJsonValues.length == 0)
          return false;

        let nounIdentityDef = Gloda._nounIDToDef[Gloda.NOUN_IDENTITY];
        let references = aReferencesByNounID[nounIdentityDef.id];
        if (references === undefined)
          references = aReferencesByNounID[nounIdentityDef.id] = {};

        for (let tupe of aJsonValues) {
          let [originIdentityID, targetIdentityID] = tupe;
          if (!(originIdentityID in references))
            references[originIdentityID] = null;
          if (!(targetIdentityID in references))
            references[targetIdentityID] = null;
        }

        return true;
      },
      resolveObjDependencies: function(aJsonValues, aReferencesByNounID,
          aInverseReferencesByNounID) {
        let references =
          aReferencesByNounID[Gloda.NOUN_IDENTITY];

        let results = [];
        for (let tupe of aJsonValues) {
          let [originIdentityID, targetIdentityID] = tupe;
          results.push([references[originIdentityID],
                        references[targetIdentityID]]);
        }

        return results;
      },
      toJSON: function (aIdentityTuple) {
        return [aIdentityTuple[0].id, aIdentityTuple[1].id];
      },
      toParamAndValue: function(aIdentityTuple) {
        return [aIdentityTuple[0].id, aIdentityTuple[1].id];
      }}, this.NOUN_PARAM_IDENTITY);

    GlodaDatastore.getAllAttributes();
  },

  /**
   * Create accessor functions to 'bind' an attribute to underlying normalized
   *  attribute storage, as well as creating the appropriate query object
   *  constraint helper functions.  This name is somewhat of a misnomer because
   *  special attributes are not 'bound' (because specific/non-generic per-class
   *  code provides the properties) but still depend on this method to
   *  establish their constraint helper methods.
   *
   * @XXX potentially rename to not suggest binding is required.
   */
  _bindAttribute: function gloda_ns_bindAttr(aAttrDef, aSubjectNounDef) {
    let objectNounDef = aAttrDef.objectNounDef;

    // -- the query constraint helpers
    if (aSubjectNounDef.queryClass !== undefined) {
      let constrainer;
      let canQuery = true;
      if (aAttrDef.special == this.kSpecialFulltext) {
        constrainer = function() {
          let constraint = [GlodaDatastore.kConstraintFulltext, aAttrDef];
          for (let iArg = 0; iArg < arguments.length; iArg++) {
            constraint.push(arguments[iArg]);
          }
          this._constraints.push(constraint);
          return this;
        };
      }
      else if (aAttrDef.canQuery || aAttrDef.attributeName.startsWith("_")) {
        constrainer = function() {
          let constraint = [GlodaDatastore.kConstraintIn, aAttrDef];
          for (let iArg = 0; iArg < arguments.length; iArg++) {
            constraint.push(arguments[iArg]);
          }
          this._constraints.push(constraint);
          return this;
        };
      } else {
        constrainer = function() {
          throw new Error(
              "Cannot query on attribute "+aAttrDef.attributeName
            + " because its canQuery parameter hasn't been set to true."
            + " Reading the comments about Gloda.defineAttribute may be a"
            + " sensible thing to do now.");
        }
        canQuery = false;
      }

      aSubjectNounDef.queryClass.prototype[aAttrDef.boundName] = constrainer;

      // Don't bind extra query-able attributes if we're unable to perform a
      // search on the attribute.
      if (!canQuery)
        return;

      // - ranged value helper: fooRange
      if (objectNounDef.continuous) {
        // takes one or more tuples of [lower bound, upper bound]
        let rangedConstrainer = function() {
          let constraint = [GlodaDatastore.kConstraintRanges, aAttrDef];
          for (let iArg = 0; iArg < arguments.length; iArg++ ) {
            constraint.push(arguments[iArg]);
          }
          this._constraints.push(constraint);
          return this;
        };

        aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + "Range"] =
          rangedConstrainer;
      }

      // - string LIKE helper for special on-row attributes: fooLike
      // (it is impossible to store a string as an indexed attribute, which is
      //  why we do this for on-row only.)
      if (aAttrDef.special == this.kSpecialString) {
        let likeConstrainer = function() {
          let constraint = [GlodaDatastore.kConstraintStringLike, aAttrDef];
          for (let iArg = 0; iArg < arguments.length; iArg++) {
            constraint.push(arguments[iArg]);
          }
          this._constraints.push(constraint);
          return this;
        };

        aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + "Like"] =
          likeConstrainer;
      }

      // - Custom helpers provided by the noun type...
      if ("queryHelpers" in objectNounDef) {
        for (let name in objectNounDef.queryHelpers) {
          let helper = objectNounDef.queryHelpers[name];
          // we need a new closure...
          let helperFunc = helper;
          aSubjectNounDef.queryClass.prototype[aAttrDef.boundName + name] =
            function() {
              return helperFunc.call(this, aAttrDef, arguments);
            };
        }
      }
    }
  },

  /**
   * Names of attribute-specific localized strings and the JS attribute they are
   *  exposed as in the attribute's "strings" attribute (if the provider has a
   *  string bundle exposed on its "strings" attribute).  They are rooted at
   *  "gloda.SUBJECT-NOUN-NAME.attr.ATTR-NAME.*".
   *
   * Please consult the localization notes in gloda.properties to understand
   *  what these are used for.
   */
  _ATTR_LOCALIZED_STRINGS: {
    /* - Faceting */
    facetNameLabel: "facetNameLabel",
    includeLabel: "includeLabel",
    excludeLabel: "excludeLabel",
    remainderLabel: "remainderLabel",
    mustMatchLabel: "mustMatchLabel",
    cantMatchLabel: "cantMatchLabel",
    mayMatchLabel: "mayMatchLabel",
    mustMatchNoneLabel: "mustMatchNoneLabel",
    mustMatchSomeLabel: "mustMatchSomeLabel",
    mayMatchAnyLabel: "mayMatchAnyLabel",
  },
  /**
   * Define an attribute and all its meta-data.  Takes a single dictionary as
   *  its argument, with the following required properties:
   *
   * @param aAttrDef.provider The object instance providing a 'process' method.
   * @param aAttrDef.extensionName The name of the extension providing these
   *     attributes.
   * @param aAttrDef.attributeType The type of attribute, one of the values from
   *     the kAttr* enumeration.
   * @param aAttrDef.attributeName The name of the attribute, which also doubles
   *     as the bound property name if you pass 'bind' a value of true.  You are
   *     responsible for avoiding collisions, which presumably will mean
   *     checking/updating a wiki page in the future, or just prefixing your
   *     attribute name with your extension name or something like that.
   * @param aAttrDef.bind Should this attribute be 'bound' as a convenience
   *     attribute on the subject's object (true/false)?  For example, with an
   *     attributeName of "foo" and passing true for 'bind' with a subject noun
   *     of NOUN_MESSAGE, GlodaMessage instances will expose a "foo" getter that
   *     returns the value of the attribute.  If 'singular' is true, this means
   *     an instance of the object class corresponding to the noun type or null
   *     if the attribute does not exist.  If 'singular' is false, this means a
   *     list of instances of the object class corresponding to the noun type,
   *     where the list may be empty if no instances of the attribute are
   *     present.
   * @param aAttrDef.bindName Optional override of attributeName for purposes of
   *     the binding property's name.
   * @param aAttrDef.singular Is the attribute going to happen at most once
   *     (true), or potentially multiple times (false).  This affects whether
   *     the binding returns a list or just a single item (which is null when
   *     the attribute is not present).
   * @param [aAttrDef.emptySetIsSignificant=false] Should we
   * @param aAttrDef.subjectNouns A list of object types (NOUNs) that this
   *     attribute can be set on.  Each element in the list should be one of the
   *     NOUN_* constants or a dynamically registered noun type.
   * @param aAttrDef.objectNoun The object type (one of the NOUN_* constants or
   *     a dynamically registered noun types) that is the 'object' in the
   *     traditional RDF triple.  More pragmatically, in the database row used
   *     to represent an attribute, we store the subject (ex: message ID),
   *     attribute ID, and an integer which is the integer representation of the
   *     'object' whose type you are defining right here.
   */
  defineAttribute: function gloda_ns_defineAttribute(aAttrDef) {
    // ensure required properties exist on aAttrDef
    if (!("provider" in aAttrDef) ||
        !("extensionName" in aAttrDef) ||
        !("attributeType" in aAttrDef) ||
        !("attributeName" in aAttrDef) ||
        !("singular" in aAttrDef) ||
        !("subjectNouns" in aAttrDef) ||
        !("objectNoun" in aAttrDef))
      // perhaps we should have a list of required attributes, perchance with
      //  and explanation of what it holds, and use that to be friendlier?
      throw Error("You omitted a required attribute defining property, please" +
                  " consult the documentation as penance.");

    // -- Fill in defaults
    if (!("emptySetIsSignificant" in aAttrDef))
      aAttrDef.emptySetIsSignificant = false;

    if (!("canQuery" in aAttrDef))
      aAttrDef.canQuery = aAttrDef.facet ? true : false;

    // return if the attribute has already been defined
    if (aAttrDef.dbDef)
      return aAttrDef;

    // - first time we've seen a provider init logic
    if (!(aAttrDef.provider.providerName in this._attrProviders)) {
      this._attrProviders[aAttrDef.provider.providerName] = [];
      if (aAttrDef.provider.contentWhittle)
        whittlerRegistry.registerWhittler(aAttrDef.provider);
    }

    let compoundName = aAttrDef.extensionName + ":" + aAttrDef.attributeName;
    // -- Database Definition
    let attrDBDef;
    if (compoundName in GlodaDatastore._attributeDBDefs) {
      // the existence of the GlodaAttributeDBDef means that either it has
      //  already been fully defined, or has been loaded from the database but
      //  not yet 'bound' to a provider (and had important meta-info that
      //  doesn't go in the db copied over)
      attrDBDef = GlodaDatastore._attributeDBDefs[compoundName];
    }
    // we need to create the attribute definition in the database
    else {
      let attrID = null;
      attrID = GlodaDatastore._createAttributeDef(aAttrDef.attributeType,
                                                  aAttrDef.extensionName,
                                                  aAttrDef.attributeName,
                                                  null);

      attrDBDef = new GlodaAttributeDBDef(GlodaDatastore, attrID, compoundName,
        aAttrDef.attributeType, aAttrDef.extensionName, aAttrDef.attributeName);
      GlodaDatastore._attributeDBDefs[compoundName] = attrDBDef;
      GlodaDatastore._attributeIDToDBDefAndParam[attrID] = [attrDBDef, null];
    }

    aAttrDef.dbDef = attrDBDef;
    attrDBDef.attrDef = aAttrDef;

    aAttrDef.id = aAttrDef.dbDef.id;

    if ("bindName" in aAttrDef)
      aAttrDef.boundName = aAttrDef.bindName;
    else
      aAttrDef.boundName = aAttrDef.attributeName;

    aAttrDef.objectNounDef = this._nounIDToDef[aAttrDef.objectNoun];
    aAttrDef.objectNounDef.objectNounOfAttributes.push(aAttrDef);

    // -- Facets
    function normalizeFacetDef(aFacetDef) {
      if (!("groupIdAttr" in aFacetDef))
        aFacetDef.groupIdAttr = aAttrDef.objectNounDef.idAttr;
      if (!("groupComparator" in aFacetDef))
        aFacetDef.groupComparator = aAttrDef.objectNounDef.comparator;
      if (!("filter" in aFacetDef))
        aFacetDef.filter = null;
    }
    // No facet attribute means no facet desired; set an explicit null so that
    //  code can check without doing an "in" check.
    if (!("facet" in aAttrDef))
      aAttrDef.facet = null;
    // Promote "true" facet values to the defaults.  Where attributes have
    //  specified values, make sure we fill in any missing defaults.
    else {
      if (aAttrDef.facet == true) {
        aAttrDef.facet = {
          type: "default",
          groupIdAttr: aAttrDef.objectNounDef.idAttr,
          groupComparator: aAttrDef.objectNounDef.comparator,
          filter: null,
        };
      }
      else {
        normalizeFacetDef(aAttrDef.facet);
      }
    }
    if ("extraFacets" in aAttrDef) {
      for (let facetDef of aAttrDef.extraFacets) {
        normalizeFacetDef(facetDef);
      }
    }

    function gatherLocalizedStrings(aBundle, aPropRoot, aStickIn) {
      for (let propName in Gloda._ATTR_LOCALIZED_STRINGS) {
        let attrName = Gloda._ATTR_LOCALIZED_STRINGS[propName];
        try {
          aStickIn[attrName] = aBundle.get(aPropRoot + propName);
        }
        catch (ex) {
          // do nothing.  nsIStringBundle throws exceptions because it is a
          //  standard nsresult type of API and our helper buddy does nothing
          //  to help us.  (StringBundle.js, that is.)
        }
      }
    }

    // -- L10n.
    // If the provider has a string bundle, populate a "strings" attribute with
    //  our standard attribute strings that can be UI exposed.
    if (("strings" in aAttrDef.provider) && (aAttrDef.facet)) {
      let bundle = aAttrDef.provider.strings;

      // -- attribute strings
      let attrStrings = aAttrDef.facet.strings = {};
      // we use the first subject the attribute applies to as the basis of
      //  where to get the string from.  Mainly because we currently don't have
      //  any attributes with multiple subjects nor a use-case where we expose
      //  multiple noun types via the UI.  (Just messages right now.)
      let canonicalSubject = this._nounIDToDef[aAttrDef.subjectNouns[0]];
      let propRoot = "gloda." + canonicalSubject.name + ".attr." +
                       aAttrDef.attributeName + ".";
      gatherLocalizedStrings(bundle, propRoot, attrStrings);

      // -- alias strings for synthetic facets
      if ("extraFacets" in aAttrDef) {
        for (let facetDef of aAttrDef.extraFacets) {
          facetDef.strings = {};
          let aliasPropRoot = "gloda." + canonicalSubject.name + ".attr." +
                                facetDef.alias + ".";
          gatherLocalizedStrings(bundle, aliasPropRoot, facetDef.strings);
        }
      }
    }

    // -- Subject Noun Binding
    for (let iSubject = 0; iSubject < aAttrDef.subjectNouns.length;
           iSubject++) {
      let subjectType = aAttrDef.subjectNouns[iSubject];
      let subjectNounDef = this._nounIDToDef[subjectType];
      this._bindAttribute(aAttrDef, subjectNounDef);

      // update the provider maps...
      if (this._attrProviderOrderByNoun[subjectType]
              .indexOf(aAttrDef.provider) == -1) {
        this._attrProviderOrderByNoun[subjectType].push(aAttrDef.provider);
        if (aAttrDef.provider.optimize)
          this._attrOptimizerOrderByNoun[subjectType].push(aAttrDef.provider);
        this._attrProvidersByNoun[subjectType][aAttrDef.provider] = [];
      }
      this._attrProvidersByNoun[subjectType][aAttrDef.provider].push(aAttrDef);

      subjectNounDef.attribsByBoundName[aAttrDef.boundName] = aAttrDef;
      if (aAttrDef.domExpose)
        subjectNounDef.domExposeAttribsByBoundName[aAttrDef.boundName] =
          aAttrDef;

      if (aAttrDef.special & this.kSpecialColumn)
        subjectNounDef.specialLoadAttribs.push(aAttrDef);

      // if this is a parent column attribute, make note of it so that if we
      //  need to do an inverse references lookup, we know what column we are
      //  issuing against.
      if (aAttrDef.special === this.kSpecialColumnParent) {
        subjectNounDef.parentColumnAttr = aAttrDef;
      }

      if (aAttrDef.objectNounDef.tableName ||
          aAttrDef.objectNounDef.contributeObjDependencies) {
        subjectNounDef.hasObjDependencies = true;
      }
    }

    this._attrProviders[aAttrDef.provider.providerName].push(aAttrDef);
    return aAttrDef;
  },

  /**
   * Retrieve the attribute provided by the given extension with the given
   *  attribute name.  The original idea was that plugins would effectively
   *  name-space attributes, helping avoid collisions.  Since we are leaning
   *  towards using binding heavily, this doesn't really help, as the collisions
   *  will just occur on the attribute name instead.  Also, this can turn
   *  extensions into liars as name changes/moves to core/etc. happen.
   * @TODO consider removing the extension name argument parameter requirement
   */
  getAttrDef: function gloda_ns_getAttrDef(aPluginName, aAttrName) {
    let compoundName = aPluginName + ":" + aAttrName;
    return GlodaDatastore._attributeDBDefs[compoundName];
  },

  /**
   * Create a new query instance for the given noun-type.  This provides
   *  a generic way to provide constraint-based queries of any first-class
   *  nouns supported by the system.
   *
   * The idea is that every attribute on an object can be used to express
   *  a constraint on the query object.  Constraints implicitly 'AND' together,
   *  but providing multiple arguments to a constraint function results in an
   *  'OR'ing of those values.  Additionally, you can call or() on the returned
   *  query to create an alternate query that is effectively a giant OR against
   *  all the constraints you create on the main query object (or any other
   *  alternate queries returned by or()).  (Note: there is no nesting of these
   *  alternate queries. query.or().or() is equivalent to query.or())
   * For each attribute, there is a constraint with the same name that takes
   *  one or more arguments.  The arguments represent a set of OR values that
   *  objects matching the query can have.  (If you want the constraint
   *  effectively ANDed together, just invoke the constraint function
   *  multiple times.)  For example, newQuery(NOUN_PERSON).age(25) would
   *  constraint to all the people aged 25, while age(25, 26) would constrain
   *  to all the people age 25 or 26.
   * For each attribute with a 'continuous' noun, there is a constraint with the
   *  attribute name with "Range" appended.  It takes two arguments which are an
   *  inclusive lower bound and an inclusive lower bound for values in the
   *  range.  If you would like an open-ended range on either side, pass null
   *  for that argument.  If you would like to specify multiple ranges that
   *  should be ORed together, simply pass additional (pairs of) arguments.
   *  For example, newQuery(NOUN_PERSON).age(25,100) would constraint to all
   *  the people who are >= 25 and <= 100.  Likewise age(25, null) would just
   *  return all the people who are 25 or older.  And age(25,30,35,40) would
   *  return people who are either 25-30 or 35-30.
   * There are also full-text constraint columns.  In a nutshell, their
   *  arguments are the strings that should be passed to the SQLite FTS3
   *  MATCH clause.
   *
   * @param aNounID The (integer) noun-id of the noun you want to query on.
   * @param aOptions an optional dictionary of query options, see the GlodaQuery
   *     class documentation.
   */
  newQuery: function gloda_ns_newQuery(aNounID, aOptions) {
    let nounDef = this._nounIDToDef[aNounID];
    return new nounDef.queryClass(aOptions);
  },

  /**
   * Create a collection/query for the given noun-type that only matches the
   *  provided items.  This is to be used when you have an explicit set of items
   *  that you would still like to receive updates for.
   */
  explicitCollection: function gloda_ns_explicitCollection(aNounID, aItems) {
    let nounDef = this._nounIDToDef[aNounID];
    let collection = new GlodaCollection(nounDef, aItems, null, null);
    let query = new nounDef.explicitQueryClass(collection);
    collection.query = query;
    GlodaCollectionManager.registerCollection(collection);
    return collection;
  },

  /**
   * Debugging 'wildcard' collection creation support.  A wildcard collection
   *  will 'accept' any new item instances presented to the collection manager
   *  as new.  The result is that it allows you to be notified as new items
   *  as they are indexed, existing items as they are loaded from the database,
   *  etc.
   * Because the items are added to the collection without limit, this will
   *  result in a leak if you don't do something to clean up after the
   *  collection.  (Forgetting about the collection will suffice, as it is still
   *  weakly held.)
   */
  _wildcardCollection: function gloda_ns_wildcardCollection(aNounID, aItems) {
    let nounDef = this._nounIDToDef[aNounID];
    let collection = new GlodaCollection(nounDef, aItems, null, null);
    let query = new nounDef.wildcardQueryClass(collection);
    collection.query = query;
    GlodaCollectionManager.registerCollection(collection);
    return collection;
  },

  /**
   * Attribute providers attempting to index something that experience a fatal
   *  problem should throw one of these.  For example:
   *  "throw new Gloda.BadItemContentsError('Message lacks an author.');".
   *
   * We're not really taking advantage of this yet, but it's a good idea.
   */
  BadItemContentsError: BadItemContentsError,

  /**
   * Populate a gloda representation of an item given the thus-far built
   *  representation, the previous representation, and one or more raw
   *  representations.  The attribute providers/optimizers for the given noun
   *  type are invoked, allowing them to contribute/alter things.  Following
   *  that, we build and persist our attribute representations.
   *
   * The result of the processing ends up with attributes in 3 different forms:
   * - Database attribute rows (to be added and removed).
   * - In-memory representation.
   * - JSON-able representation.
   *
   * @param aItem The noun instance you want processed.
   * @param aRawReps A dictionary that we pass to the attribute providers.
   *     There is a(n implied) contract between the caller of grokNounItem for a
   *     given noun type and the attribute providers for that noun type, and we
   *     have nothing to do with it OTHER THAN inserting a 'trueGlodaRep'
   *     value into it.  In the event of reindexing an existing object, the
   *     gloda representation we pass to the indexers is actually a clone that
   *     allows the asynchronous indexers to mutate the object without
   *     causing visible changes in the existing representation of the gloda
   *     object.  We patch the changes back onto the original item atomically
   *     once indexing completes.  The 'trueGlodaRep' is then useful for
   *     objects that hang off of the gloda instance that need a reference
   *     back to their containing object for API convenience purposes.
   * @param aIsConceptuallyNew Is the item "new" in the sense that it would
   *     never have been visible from within user code?  This translates into
   *     whether this should trigger an itemAdded notification or an
   *     itemModified notification.
   * @param aIsRecordNew Is the item "new" in the sense that we should INSERT
   *     a record rather than UPDATE-ing a record.  For example, when dealing
   *     with messages where we may have a ghost, the ghost message is not a
   *     new record, but is conceptually new.
   * @param aCallbackHandle The GlodaIndexer-style callback handle that is being
   *     used to drive this processing in an async fashion.  (See
   *     GlodaIndexer._callbackHandle).
   * @param aDoCache Should we allow this item to be contributed to its noun
   *     cache?
   */
  grokNounItem: function* gloda_ns_grokNounItem(aItem, aRawReps,
      aIsConceptuallyNew, aIsRecordNew, aCallbackHandle, aDoCache) {
    let itemNounDef = aItem.NOUN_DEF;
    let attribsByBoundName = itemNounDef.attribsByBoundName;

    this._log.info(" ** grokNounItem: " + itemNounDef.name);

    let addDBAttribs = [];
    let removeDBAttribs = [];

    let jsonDict = {};

    let aOldItem;
    aRawReps.trueGlodaRep = aItem;
    if (aIsConceptuallyNew) // there is no old item if we are new.
      aOldItem = {};
    else {
      aOldItem = aItem;
      // we want to create a clone of the existing item so that we can know the
      //  deltas that happened for indexing purposes
      aItem = aItem._clone();
    }

    // Have the attribute providers directly set properties on the aItem
    let attrProviders = this._attrProviderOrderByNoun[itemNounDef.id];
    for (let iProvider = 0; iProvider < attrProviders.length; iProvider++) {
      this._log.info("  * provider: " + attrProviders[iProvider].providerName);
      yield aCallbackHandle.pushAndGo(
        attrProviders[iProvider].process(aItem, aRawReps, aIsConceptuallyNew,
                                         aCallbackHandle));
    }

    let attrOptimizers = this._attrOptimizerOrderByNoun[itemNounDef.id];
    for (let iProvider = 0; iProvider < attrOptimizers.length; iProvider++) {
      this._log.info("  * optimizer: " + attrOptimizers[iProvider].providerName);
      yield aCallbackHandle.pushAndGo(
        attrOptimizers[iProvider].optimize(aItem, aRawReps, aIsConceptuallyNew,
                                           aCallbackHandle));
    }
    this._log.info(" ** done with providers.");

    // Iterate over the attributes on the item
    for (let key of Object.keys(aItem)) {
      let value = aItem[key];
      // ignore keys that start with underscores, they are private and not
      //  persisted by our attribute mechanism.  (they are directly handled by
      //  the object implementation.)
      if (key.startsWith("_"))
        continue;
      // find the attribute definition that corresponds to this key
      let attrib = attribsByBoundName[key];
      // if there's no attribute, that's not good, but not horrible.
      if (attrib === undefined) {
        this._log.warn("new proc ignoring attrib: " + key);
        continue;
      }

      let attribDB = attrib.dbDef;
      let objectNounDef = attrib.objectNounDef;

      // - translate for our JSON rep
      if (attrib.singular) {
        if (objectNounDef.toJSON)
          jsonDict[attrib.id] = objectNounDef.toJSON(value);
        else
          jsonDict[attrib.id] = value;
      }
      else {
        if (objectNounDef.toJSON) {
          let toJSON = objectNounDef.toJSON;
          jsonDict[attrib.id] = [];
          for (let [, subValue] in Iterator(value)) {
            jsonDict[attrib.id].push(toJSON(subValue));
          }
        }
        else
          jsonDict[attrib.id] = value;
      }

      let oldValue = aOldItem[key];

      // the 'old' item is still the canonical one; update it
      // do the update now, because we may skip operations on addDBAttribs and
      //  removeDBattribs, if the attribute is not to generate entries in
      //  messageAttributes
      if (oldValue !== undefined || !aIsConceptuallyNew)
        aOldItem[key] = value;

      // the new canQuery property has to be set to true to generate entries
      // in the messageAttributes table. Any other truthy value (like a non
      // empty string), will still make the message query-able but without
      // using the database.
      if (attrib.canQuery !== true) {
        continue;
      }

      // - database index attributes

      // perform a delta analysis against the old value, if we have one
      if (oldValue !== undefined) {
        // in the singular case if they don't match, it's one add and one remove
        if (attrib.singular) {
          // test for identicality, failing that, see if they have explicit
          //  equals support.
          if ((value !== oldValue) &&
              (!value.equals || !value.equals(oldValue))) {
            addDBAttribs.push(attribDB.convertValuesToDBAttributes([value])[0]);
            removeDBAttribs.push(
              attribDB.convertValuesToDBAttributes([oldValue])[0]);
          }
        }
        // in the plural case, we have to figure the deltas accounting for
        //  possible changes in ordering (which is insignificant from an
        //  indexing perspective)
        // some nouns may not meet === equivalence needs, so must provide a
        //  custom computeDelta method to help us out
        else if (objectNounDef.computeDelta) {
          let [valuesAdded, valuesRemoved] =
            objectNounDef.computeDelta(value, oldValue);
          // convert the values to database-style attribute rows
          addDBAttribs.push.apply(addDBAttribs,
            attribDB.convertValuesToDBAttributes(valuesAdded));
          removeDBAttribs.push.apply(removeDBAttribs,
            attribDB.convertValuesToDBAttributes(valuesRemoved));
        }
        else {
          // build a map of the previous values; we will delete the values as
          //  we see them so that we will know what old values are no longer
          //  present in the current set of values.
          let oldValueMap = {};
          for (let anOldValue of oldValue) {
            // remember, the key is just the toString'ed value, so we need to
            //  store and use the actual value as the value!
            oldValueMap[anOldValue] = anOldValue;
          }
          // traverse the current values...
          let valuesAdded = [];
          for (let curValue of value) {
            if (curValue in oldValueMap)
              delete oldValueMap[curValue];
            else
              valuesAdded.push(curValue);
          }
          // anything still on oldValueMap was removed.
          let valuesRemoved = Object.keys(oldValueMap).
            map(key => oldValueMap[key]);
          // convert the values to database-style attribute rows
          addDBAttribs.push.apply(addDBAttribs,
            attribDB.convertValuesToDBAttributes(valuesAdded));
          removeDBAttribs.push.apply(removeDBAttribs,
            attribDB.convertValuesToDBAttributes(valuesRemoved));
        }

        // Add/remove the empty set indicator as appropriate.
        if (attrib.emptySetIsSignificant) {
          // if we are now non-zero but previously were zero, remove.
          if (value.length && !oldValue.length)
            removeDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
          // if we are now zero length but previously were not, add
          else if (!value.length && oldValue.length)
            addDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
        }
      }
      // no old value, all values are new
      else {
        // add the db reps on the new values
        if (attrib.singular)
          value = [value];
        addDBAttribs.push.apply(addDBAttribs,
                                attribDB.convertValuesToDBAttributes(value));
        // Add the empty set indicator for the attribute id if appropriate.
        if (!value.length && attrib.emptySetIsSignificant)
          addDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
      }
    }

    // Iterate over any remaining values in old items for purge purposes.
    for (let key of Object.keys(aOldItem)) {
      let value = aOldItem[key];
      // ignore keys that start with underscores, they are private and not
      //  persisted by our attribute mechanism.  (they are directly handled by
      //  the object implementation.)
      if (key.startsWith("_"))
        continue;
      // ignore things we saw in the new guy
      if (key in aItem)
        continue;

      // find the attribute definition that corresponds to this key
      let attrib = attribsByBoundName[key];
      // if there's no attribute, that's not good, but not horrible.
      if (attrib === undefined) {
        continue;
      }

      // delete these from the old item, as the old item is canonical, and
      //  should no longer have these values
      delete aOldItem[key];

      if (attrib.canQuery !== true) {
        this._log.debug("Not inserting attribute "+attrib.attributeName
            +" into the db, since we don't plan on querying on it");
        continue;
      }

      if (attrib.singular)
        value = [value];
      let attribDB = attrib.dbDef;
      removeDBAttribs.push.apply(removeDBAttribs,
                                 attribDB.convertValuesToDBAttributes(value));
      // remove the empty set marker if there should have been one
      if (!value.length && attrib.emptySetIsSignificant)
        removeDBAttribs.push([GlodaDatastore.kEmptySetAttrId, attribDB.id]);
    }

    aItem._jsonText = JSON.stringify(jsonDict);
    this._log.debug("  json text: " + aItem._jsonText);

    if (aIsRecordNew) {
      this._log.debug(" inserting item");
      itemNounDef.objInsert.call(itemNounDef.datastore, aItem);
    }
    else {
      this._log.debug(" updating item");
      itemNounDef.objUpdate.call(itemNounDef.datastore, aItem);
    }

    this._log.debug(" adjusting attributes, add: " + addDBAttribs + " rem: " +
        removeDBAttribs);
    itemNounDef.dbAttribAdjuster.call(itemNounDef.datastore, aItem,
      addDBAttribs, removeDBAttribs);

    if (!aIsConceptuallyNew && ("_declone" in aOldItem))
      aOldItem._declone(aItem);

    // Cache ramifications...
    if (aDoCache === undefined || aDoCache) {
      if (aIsConceptuallyNew)
        GlodaCollectionManager.itemsAdded(aItem.NOUN_ID, [aItem]);
      else
        GlodaCollectionManager.itemsModified(aOldItem.NOUN_ID, [aOldItem]);
    }

    this._log.debug(" done grokking.");

    yield this.kWorkDone;
  },

  /**
   * Processes a list of noun instances for their score within a given context.
   *  This is primarily intended for use by search ranking mechanisms, but could
   *  be used elsewhere too.  (It does, however, depend on the complicity of the
   *  score method implementations to not get confused.)
   *
   * @param aItems The non-empty list of items to score.
   * @param aContext A noun-specific dictionary that we just pass to the funcs.
   * @param aExtraScoreFuncs A list of extra scoring functions to apply.
   * @returns A list of integer scores equal in length to aItems.
   */
  scoreNounItems: function gloda_ns_grokNounItem(aItems, aContext,
                                                 aExtraScoreFuncs) {
    let scores = [];
    // bail if there is nothing to score
    if (!aItems.length)
      return scores;

    let itemNounDef = aItems[0].NOUN_DEF;
    if (aExtraScoreFuncs == null)
      aExtraScoreFuncs = [];

    for (let item of aItems) {
      let score = 0;
      let attrProviders = this._attrProviderOrderByNoun[itemNounDef.id];
      for (let iProvider = 0; iProvider < attrProviders.length; iProvider++) {
        let provider = attrProviders[iProvider];
        if (provider.score)
          score += provider.score(item);
      }
      for (let [, extraScoreFunc] in Iterator(aExtraScoreFuncs))
        score += extraScoreFunc(item, aContext);
      scores.push(score);
    }

    return scores;
  }
};

/* and initialize the Gloda object/NS before we return... */
try {
  Gloda._init();
}
catch (ex) {
  Gloda._log.debug("Exception during Gloda init (" + ex.fileName + ":" +
                   ex.lineNumber + "): " + ex);
};
/* but don't forget that we effectively depend on everybody.js too, and
   currently on our importer to be importing that if they need us fully armed
   and operational. */
