/* 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 = ['GlodaABIndexer', 'GlodaABAttrs'];

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

Cu.import("resource:///modules/gloda/collection.js");
Cu.import("resource:///modules/gloda/datastore.js");
Cu.import("resource:///modules/gloda/gloda.js");
Cu.import("resource:///modules/gloda/indexer.js");
Cu.import("resource:///modules/gloda/log4moz.js");
Cu.import("resource:///modules/gloda/noun_freetag.js");
Cu.import("resource:///modules/gloda/utils.js");
Cu.import("resource:///modules/mailServices.js");


var GlodaABIndexer = {
  _log: null,

  name: "index_ab",
  enable: function() {
    if (this._log == null)
      this._log =  Log4Moz.repository.getLogger("gloda.index_ab");

    MailServices.ab.addAddressBookListener(this,
                                           Ci.nsIAbListener.itemAdded |
                                           Ci.nsIAbListener.itemChanged |
                                           Ci.nsIAbListener.directoryItemRemoved);
  },

  disable: function() {
    MailServices.ab.removeAddressBookListener(this);
  },

  // it's a getter so we can reference 'this'
  get workers() {
    return [
      ["ab-card", {
         worker: this._worker_index_card,
       }],
    ];
  },

  _worker_index_card: function*(aJob, aCallbackHandle) {
    let card = aJob.id;

    if (card.primaryEmail) {
      // load the identity
      let query = Gloda.newQuery(Gloda.NOUN_IDENTITY);
      query.kind("email");
      // we currently normalize all e-mail addresses to be lowercase
      query.value(card.primaryEmail.toLowerCase());
      let identityCollection = query.getCollection(aCallbackHandle);
      yield Gloda.kWorkAsync;

      if (identityCollection.items.length) {
        let identity = identityCollection.items[0];
        // force the identity to know it has an associated ab card.
        identity._hasAddressBookCard = true;

        this._log.debug("Found identity, processing card.");
        yield aCallbackHandle.pushAndGo(
            Gloda.grokNounItem(identity.contact, {card: card}, false, false,
                               aCallbackHandle));
        this._log.debug("Done processing card.");
      }
    }

    yield GlodaIndexer.kWorkDone;
  },

  initialSweep: function() {
  },

  /* ------ nsIAbListener ------ */
  /**
   * When an address book card is added, update the cached GlodaIdentity
   *  object's cached idea of whether the identity has an ab card.
   */
  onItemAdded: function ab_indexer_onItemAdded(aParentDir, aItem) {
    if (!(aItem instanceof Ci.nsIAbCard))
      return;

    this._log.debug("Received Card Add Notification");
    let identity = GlodaCollectionManager.cacheLookupOneByUniqueValue(
      Gloda.NOUN_IDENTITY, "email@" + aItem.primaryEmail.toLowerCase());
    if (identity)
      identity._hasAddressBookCard = true;
  },
  /**
   * When an address book card is added, update the cached GlodaIdentity
   *  object's cached idea of whether the identity has an ab card.
   */
  onItemRemoved: function ab_indexer_onItemRemoved(aParentDir, aItem) {
    if (!(aItem instanceof Ci.nsIAbCard))
      return;

    this._log.debug("Received Card Removal Notification");
    let identity = GlodaCollectionManager.cacheLookupOneByUniqueValue(
      Gloda.NOUN_IDENTITY, "email@" + aItem.primaryEmail.toLowerCase());
    if (identity)
      identity._hasAddressBookCard = false;

  },
  onItemPropertyChanged: function ab_indexer_onItemPropertyChanged(aItem,
      aProperty, aOldValue, aNewValue) {
    if (aProperty == null && aItem instanceof Ci.nsIAbCard) {
      this._log.debug("Received Card Change Notification");

      let card = aItem; // instanceof already QueryInterface'd for us.
      let job = new IndexingJob("ab-card", card);
      GlodaIndexer.indexJob(job);
    }
  }
};
GlodaIndexer.registerIndexer(GlodaABIndexer);

var GlodaABAttrs = {
  providerName: "gloda.ab_attr",
  _log: null,

  init: function() {
    this._log =  Log4Moz.repository.getLogger("gloda.abattrs");

    try {
      this.defineAttributes();
    }
    catch (ex) {
      this._log.error("Error in init: " + ex);
      throw ex;
    }
  },

  defineAttributes: function() {
    /* ***** Contacts ***** */
    this._attrIdentityContact = Gloda.defineAttribute({
      provider: this,
      extensionName: Gloda.BUILT_IN,
      attributeType: Gloda.kAttrDerived,
      attributeName: "identities",
      singular: false,
      special: Gloda.kSpecialColumnChildren,
      //specialColumnName: "contactID",
      storageAttributeName: "_identities",
      subjectNouns: [Gloda.NOUN_CONTACT],
      objectNoun: Gloda.NOUN_IDENTITY,
      }); // tested-by: test_attributes_fundamental
    this._attrContactName = Gloda.defineAttribute({
      provider: this,
      extensionName: Gloda.BUILT_IN,
      attributeType: Gloda.kAttrFundamental,
      attributeName: "name",
      singular: true,
      special: Gloda.kSpecialString,
      specialColumnName: "name",
      subjectNouns: [Gloda.NOUN_CONTACT],
      objectNoun: Gloda.NOUN_STRING,
      canQuery: true,
      }); // tested-by: test_attributes_fundamental
    this._attrContactPopularity = Gloda.defineAttribute({
      provider: this,
      extensionName: Gloda.BUILT_IN,
      attributeType: Gloda.kAttrDerived,
      attributeName: "popularity",
      singular: true,
      special: Gloda.kSpecialColumn,
      specialColumnName: "popularity",
      subjectNouns: [Gloda.NOUN_CONTACT],
      objectNoun: Gloda.NOUN_NUMBER,
      canQuery: true,
      }); // not-tested
    this._attrContactFrecency = Gloda.defineAttribute({
      provider: this,
      extensionName: Gloda.BUILT_IN,
      attributeType: Gloda.kAttrDerived,
      attributeName: "frecency",
      singular: true,
      special: Gloda.kSpecialColumn,
      specialColumnName: "frecency",
      subjectNouns: [Gloda.NOUN_CONTACT],
      objectNoun: Gloda.NOUN_NUMBER,
      canQuery: true,
      }); // not-tested

    /* ***** Identities ***** */
    this._attrIdentityContact = Gloda.defineAttribute({
      provider: this,
      extensionName: Gloda.BUILT_IN,
      attributeType: Gloda.kAttrDerived,
      attributeName: "contact",
      singular: true,
      special: Gloda.kSpecialColumnParent,
      specialColumnName: "contactID", // the column in the db
      idStorageAttributeName: "_contactID",
      valueStorageAttributeName: "_contact",
      subjectNouns: [Gloda.NOUN_IDENTITY],
      objectNoun: Gloda.NOUN_CONTACT,
      canQuery: true,
      }); // tested-by: test_attributes_fundamental
    this._attrIdentityKind = Gloda.defineAttribute({
      provider: this,
      extensionName: Gloda.BUILT_IN,
      attributeType: Gloda.kAttrFundamental,
      attributeName: "kind",
      singular: true,
      special: Gloda.kSpecialString,
      specialColumnName: "kind",
      subjectNouns: [Gloda.NOUN_IDENTITY],
      objectNoun: Gloda.NOUN_STRING,
      canQuery: true,
      }); // tested-by: test_attributes_fundamental
    this._attrIdentityValue = Gloda.defineAttribute({
      provider: this,
      extensionName: Gloda.BUILT_IN,
      attributeType: Gloda.kAttrFundamental,
      attributeName: "value",
      singular: true,
      special: Gloda.kSpecialString,
      specialColumnName: "value",
      subjectNouns: [Gloda.NOUN_IDENTITY],
      objectNoun: Gloda.NOUN_STRING,
      canQuery: true,
      }); // tested-by: test_attributes_fundamental

    /* ***** Contact Meta ***** */
    // Freeform tags; not explicit like thunderbird's fundamental tags.
    //  we differentiate for now because of fundamental implementation
    //  differences.
    this._attrFreeTag = Gloda.defineAttribute({
      provider: this,
      extensionName: Gloda.BUILT_IN,
      attributeType: Gloda.kAttrExplicit,
      attributeName: "freetag",
      bind: true,
      bindName: "freeTags",
      singular: false,
      subjectNouns: [Gloda.NOUN_CONTACT],
      objectNoun: Gloda.lookupNoun("freetag"),
      parameterNoun: null,
      canQuery: true,
      }); // not-tested
    // we need to find any existing bound freetag attributes, and use them to
    //  populate to FreeTagNoun's understanding
    if ("parameterBindings" in this._attrFreeTag) {
      for (let freeTagName in this._attrFreeTag.parameterBindings) {
        this._log.debug("Telling FreeTagNoun about: " + freeTagName);
        FreeTagNoun.getFreeTag(freeTagName);
      }
    }
  },

  process: function*(aContact, aRawReps, aIsNew, aCallbackHandle) {
    let card = aRawReps.card;
    if (aContact.NOUN_ID != Gloda.NOUN_CONTACT) {
      this._log.warn("Somehow got a non-contact: " + aContact);
      return; // this will produce an exception; we like.
    }

    // update the name
    if (card.displayName && card.displayName != aContact.name)
      aContact.name = card.displayName;

    aContact.freeTags = [];

    let tags = null;
    try {
      tags = card.getProperty("Categories", null);
    } catch (ex) {
      this._log.error("Problem accessing property: " + ex);
    }
    if (tags) {
      for (let tagName of tags.split(",")) {
        tagName = tagName.trim();
        if (tagName) {
          aContact.freeTags.push(FreeTagNoun.getFreeTag(tagName));
        }
      }
    }

    yield Gloda.kWorkDone;
  }
};
