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

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/StringBundle.js");

var LOG = Log4Moz.repository.getLogger("gloda.noun.mimetype");

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

var CategoryStringMap = {};

/**
 * Mime type abstraction that exists primarily so we can map mime types to
 *  integer id's.
 *
 * Instances of this class should only be retrieved via |MimeTypeNoun|; no one
 *  should ever create an instance directly.
 */
function MimeType(aID, aType, aSubType, aFullType, aCategory) {
  this._id = aID;
  this._type = aType;
  this._subType = aSubType;
  this._fullType = aFullType;
  this._category = aCategory;
}

MimeType.prototype = {
  /**
   * The integer id we have associated with the mime type.  This is stable for
   *  the lifetime of the database, which means that anything in the Gloda
   *  database can use this without fear.  Things not persisted in the database
   *  should use the actual string mime type, retrieval via |fullType|.
   */
  get id() { return this._id; },
  /**
   * The first part of the MIME type; "text/plain" gets you "text".
   */
  get type() { return this._type; },
  set fullType(aFullType) {
    if (!this._fullType) {
      this._fullType = aFullType;
      [this._type, this._subType] = this._fullType.split("/");
      this._category =
        MimeTypeNoun._getCategoryForMimeType(aFullType, this._type);
    }
  },
  /**
   * If the |fullType| is "text/plain", subType is "plain".
   */
  get subType() { return this._subType; },
  /**
   * The full MIME type; "text/plain" returns "text/plain".
   */
  get fullType() { return this._fullType; },
  toString: function () {
    return this.fullType;
  },

  /**
   * @return the category we believe this mime type belongs to.  This category
   *     name should never be shown directly to the user.  Instead, use
   *     |categoryLabel| to get the localized name for the category.  The
   *     category mapping comes from mimeTypesCategories.js.
   */
  get category() {
    return this._category;
  },
  /**
   * @return The localized label for the category from gloda.properties in the
   *     "gloda.mimetype.category.CATEGORY.label" definition using the value
   *     from |category|.
   */
  get categoryLabel() {
    return CategoryStringMap[this._category];
  }
};

/**
 * Mime type noun provider.
 *
 * The set of MIME Types is sufficiently limited that we can keep them all in
 *  memory.  In theory it is also sufficiently limited that we could use the
 *  parameter mechanism in the database.  However, it is more efficient, for
 *  both space and performance reasons, to store the specific mime type as a
 *  value.  For future-proofing reasons, we opt to use a database table to
 *  persist the mapping rather than a hard-coded list.  A preferences file or
 *  other text file would arguably suffice, but for consistency reasons, the
 *  database is not a bad thing.
 */
var MimeTypeNoun = {
  name: "mime-type",
  clazz: MimeType, // gloda supports clazz as well as class
  allowsArbitraryAttrs: false,

  _strings: new StringBundle("chrome://messenger/locale/gloda.properties"),

  // note! update test_noun_mimetype if you change our internals!
  _mimeTypes: {},
  _mimeTypesByID: {},
  TYPE_BLOCK_SIZE: 16384,
  _mimeTypeHighID: {},
  _mimeTypeRangeDummyObjects: {},
  _highID: 0,

  // we now use the exciting 'schema' mechanism of defineNoun to get our table
  //  created for us, plus some helper methods that we simply don't use.
  schema: {
    name: 'mimeTypes',
    columns: [['id', 'INTEGER PRIMARY KEY', '_id'],
              ['mimeType', 'TEXT', 'fullType']],
  },

  _init: function() {
    LOG.debug("loading MIME types");
    this._loadCategoryMapping();
    this._loadMimeTypes();
  },

  /**
   * A map from MIME type to category name.
   */
  _mimeTypeToCategory: {},
  /**
   * Load the contents of mimeTypeCategories.js and populate
   */
  _loadCategoryMapping: function MimeTypeNoun__loadCategoryMapping() {
    let mimecatNS = {};
    Cu.import("resource:///modules/gloda/mimeTypeCategories.js",
              mimecatNS);
    let mcm = mimecatNS.MimeCategoryMapping;

    let mimeTypeToCategory = this._mimeTypeToCategory;

    function procMapObj(aSubTree, aCategories) {
      for (let key in aSubTree) {
        let value = aSubTree[key];
        // Add this category to our nested categories list.  Use concat since
        //  the list will be long-lived and each list needs to be distinct.
        let categories = aCategories.concat();
        categories.push(key);

        if (categories.length == 1) {
          CategoryStringMap[key] =
            MimeTypeNoun._strings.get(
              "gloda.mimetype.category." + key + ".label");
        }

        // Is it an array? If so, just process this depth
        if (Array.isArray(value)) {
          for (let mimeTypeStr of value) {
            mimeTypeToCategory[mimeTypeStr] = categories;
          }
        }
        // it's yet another sub-tree branch
        else {
          procMapObj(value, categories);
        }
      }
    }

    procMapObj(mimecatNS.MimeCategoryMapping, []);
  },

  /**
   * Lookup the category associated with a MIME type given its full type and
   *  type.  (So, "foo/bar" and "foo" for "foo/bar".)
   */
  _getCategoryForMimeType:
      function MimeTypeNoun__getCategoryForMimeType(aFullType, aType) {
    if (aFullType in this._mimeTypeToCategory)
      return this._mimeTypeToCategory[aFullType][0];
    let wildType = aType + "/*";
    if (wildType in this._mimeTypeToCategory)
      return this._mimeTypeToCategory[wildType][0];
    return this._mimeTypeToCategory["*"][0];
  },

  /**
   * In order to allow the gloda query mechanism to avoid hitting the database,
   *  we need to either define the noun type as cachable and have a super-large
   *  cache or simply have a collection with every MIME type in it that stays
   *  alive forever.
   * This is that collection.  It is initialized by |_loadMimeTypes|.  As new
   *  MIME types are created, we add them to the collection.
   */
  _universalCollection: null,

  /**
   * Kick off a query of all the mime types in our database, leaving
   *  |_processMimeTypes| to actually do the legwork.
   */
  _loadMimeTypes: function MimeTypeNoun__loadMimeTypes() {
    // get all the existing mime types!
    let query = Gloda.newQuery(this.id);
    let nullFunc = function() {};
    this._universalCollection = query.getCollection({
      onItemsAdded: nullFunc, onItemsModified: nullFunc,
      onItemsRemoved: nullFunc,
      onQueryCompleted: function (aCollection) {
        MimeTypeNoun._processMimeTypes(aCollection.items);
      }
    }, null);
  },

  /**
   * For the benefit of our Category queryHelper, we need dummy ranged objects
   *  that cover the numerical address space allocated to the category.  We
   *  can't use a real object for the upper-bound because the upper-bound is
   *  constantly growing and there is the chance the query might get persisted,
   *  which means these values need to be long-lived.  Unfortunately, our
   *  solution to this problem (dummy objects) complicates the second case,
   *  should it ever occur.  (Because the dummy objects cannot be persisted
   *  on their own... but there are other issues that will come up that we will
   *  just have to deal with then.)
   */
  _createCategoryDummies: function (aId, aCategory) {
    let blockBottom = aId - (aId % this.TYPE_BLOCK_SIZE);
    let blockTop = blockBottom + this.TYPE_BLOCK_SIZE - 1;
    this._mimeTypeRangeDummyObjects[aCategory] = [
      new MimeType(blockBottom, "!category-dummy!", aCategory,
                   "!category-dummy!/" + aCategory, aCategory),
      new MimeType(blockTop, "!category-dummy!", aCategory,
                   "!category-dummy!/" + aCategory, aCategory)
    ];
  },

  _processMimeTypes: function MimeTypeNoun__processMimeTypes(aMimeTypes) {
    for (let mimeType of aMimeTypes) {
      if (mimeType.id > this._highID)
        this._highID = mimeType.id;
      this._mimeTypes[mimeType] = mimeType;
      this._mimeTypesByID[mimeType.id] = mimeType;

      let typeBlock = mimeType.id - (mimeType.id % this.TYPE_BLOCK_SIZE);
      let blockHighID = (mimeType.category in this._mimeTypeHighID) ?
                          this._mimeTypeHighID[mimeType.category] : undefined;
      // create the dummy range objects
      if (blockHighID === undefined)
        this._createCategoryDummies(mimeType.id, mimeType.category);
      if ((blockHighID === undefined) || mimeType.id > blockHighID)
        this._mimeTypeHighID[mimeType.category] = mimeType.id;
    }
  },

  _addNewMimeType: function MimeTypeNoun__addNewMimeType(aMimeTypeName) {
    let [typeName, subTypeName] = aMimeTypeName.split("/");
    let category = this._getCategoryForMimeType(aMimeTypeName, typeName);

    if (!(category in this._mimeTypeHighID)) {
      let nextID = this._highID - (this._highID % this.TYPE_BLOCK_SIZE) +
        this.TYPE_BLOCK_SIZE;
      this._mimeTypeHighID[category] = nextID;
      this._createCategoryDummies(nextID, category);
    }

    let nextID = ++this._mimeTypeHighID[category];

    let mimeType = new MimeType(nextID, typeName, subTypeName, aMimeTypeName,
                                category);
    if (mimeType.id > this._highID)
      this._highID = mimeType.id;

    this._mimeTypes[aMimeTypeName] = mimeType;
    this._mimeTypesByID[nextID] = mimeType;

    // As great as the gloda extension mechanisms are, we don't think it makes
    //  a lot of sense to use them in this case.  So we directly trigger object
    //  insertion without any of the grokNounItem stuff.
    this.objInsert.call(this.datastore, mimeType);
    // Since we bypass grokNounItem and its fun, we need to explicitly add the
    //  new MIME-type to _universalCollection ourselves.  Don't try this at
    //  home, kids.
    this._universalCollection._onItemsAdded([mimeType]);

    return mimeType;
  },

  /**
   * Map a mime type to a |MimeType| instance, creating it if necessary.
   *
   * @param aMimeTypeName The mime type.  It may optionally include parameters
   *     (which will be ignored).  A mime type is of the form "type/subtype".
   *     A type with parameters would look like 'type/subtype; param="value"'.
   */
  getMimeType: function MimeTypeNoun_getMimeType(aMimeTypeName) {
    // first, lose any parameters
    let semiIndex = aMimeTypeName.indexOf(";");
    if (semiIndex >= 0)
      aMimeTypeName = aMimeTypeName.substring(0, semiIndex);
    aMimeTypeName = aMimeTypeName.trim().toLowerCase();

    if (aMimeTypeName in this._mimeTypes)
      return this._mimeTypes[aMimeTypeName];
    else
      return this._addNewMimeType(aMimeTypeName);
  },

  /**
   * Query helpers contribute additional functions to the query object for the
   *  attributes that use the noun type.  For example, we define Category, so
   *  for the "attachmentTypes" attribute, "attachmentTypesCategory" would be
   *  exposed.
   */
  queryHelpers: {
    /**
     * Query for MIME type categories based on one or more MIME type objects
     *  passed in.  We want the range to span the entire block allocated to the
     *  category.
     *
     * @param aAttrDef The attribute that is using us.
     * @param aArguments The actual arguments object that
     */
    Category: function(aAttrDef, aArguments) {
      let rangePairs = [];
      // If there are no arguments then we want to fall back to the 'in'
      //  constraint which matches on any attachment.
      if (aArguments.length == 0)
        return this._inConstraintHelper(aAttrDef, []);

      for (let iArg = 0; iArg < aArguments.length; iArg++) {
        let arg = aArguments[iArg];
        rangePairs.push(MimeTypeNoun._mimeTypeRangeDummyObjects[arg.category]);
      }
      return this._rangedConstraintHelper(aAttrDef, rangePairs);
    }
  },

  comparator: function gloda_noun_mimeType_comparator(a, b) {
    if (a == null) {
      if (b == null)
        return 0;
      else
        return 1;
    }
    else if (b == null) {
      return -1;
    }
    return a.fullType.localeCompare(b.fullType);
  },

  toParamAndValue: function gloda_noun_mimeType_toParamAndValue(aMimeType) {
    return [null, aMimeType.id];
  },
  toJSON: function gloda_noun_mimeType_toJSON(aMimeType) {
    return aMimeType.id;
  },
  fromJSON: function gloda_noun_mimeType_fromJSON(aMimeTypeID) {
    return this._mimeTypesByID[aMimeTypeID];
  },
};
Gloda.defineNoun(MimeTypeNoun, Gloda.NOUN_MIME_TYPE);
try {
MimeTypeNoun._init();
} catch (ex) {
  LOG.error("problem init-ing: " + ex.fileName + ":" + ex.lineNumber + ": " + ex);
}
