//
// BEGIN FLOCK GPL
//
// Copyright Flock Inc. 2005-2009
// http://flock.com
//
// This file may be used under the terms of the
// GNU General Public License Version 2 or later (the "GPL"),
// http://www.gnu.org/licenses/gpl.html
//
// Software distributed under the License is distributed on an "AS IS" basis,
// WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
// for the specific language governing rights and limitations under the
// License.
//
// END FLOCK GPL
//
// This code is based on the Javascript part of the proposed patch
// to Mozilla found here:
//
//    https://bugzilla.mozilla.org/show_bug.cgi?id=71689
//
// Bugzilla Bug 71689 - Need helper utilities for JS component authors
//  "They suffer without a generic module implementation, and the
//   cutting-and-pasting breaks my little heart."  (a-yup)
//
// All objects in here become properties on the 'FlockXPCOMUtils' object.

const CC = Components.classes;
const CI = Components.interfaces;
const CR = Components.results;

var EXPORTED_SYMBOLS = [ "FlockXPCOMUtils" ];

debug("*** loading FlockXPCOMUtils\n");

var FlockXPCOMUtils = {

  debug: false,

/**
 * Creates an NSGetModule function that returns the result of calling
 * genericModule with the same parameters.
 */
  generateNSGetModule: function FlockXPCOMUtils_generateNSGetModule(aName, aComponentsArray) {
    return function FlockXPCOMUtils_NSGetModule(aCompMgr, aFileSpec) {
      return new FlockXPCOMUtils.genericModule(aName, aComponentsArray);
    };
  },

/**
 * Creates a generic Javascript XPCOM module to contain one or more XPCOM
 * components also written in Javascript.
 *
 * @param aName (in) A descriptive name for the module. Only used for
 *                   identifying purposes in debug messages.
 * @param aComponentsArray (in) 
 *        
 * @return An object that implements the methods required to participate in
 *         XPCOM: registerSelf, unregisterSelf, getClassObject, canUnload.
 */
    genericModule: function FlockXPCOMUtils_genericModule(aName, aComponentsArray) {

    // the first argument is the module name (used for debugging only)
    this.moduleName = aName;

    // and the second is an array of components
    this.classes = [];
    for each (let component in aComponentsArray) {
      this.classes.push(FlockXPCOMUtils.classDescForComponent(component));
    }

    // use FlockXPCOMUtils as the default for printing debugging statements
    this.debug = FlockXPCOMUtils.debug;

    // override printDebug() to make messages slightly more informative
    this.printDebug = function genericModule_printDebug(message) {
      if (this.debug) {
        FlockXPCOMUtils.printDebug("FlockXPCOMUtils Mod: " + this.moduleName + ": " + message);
      }
    };

    if (this.classes.length === 0) {
      this.printDebug("(WARNING) no components given to genericModule");
    }

    this.registerSelf = function genericModule_registerSelf(aCompMgr, aFileSpec, aLocation, aType) {
      this.printDebug("entering registerSelf...");
      var registrar = aCompMgr.QueryInterface(CI.nsIComponentRegistrar);

      for each (let classDesc in this.classes) {
        this.printDebug(" registering component '" + classDesc.className + "'");
        registrar.registerFactoryLocation(classDesc.classID,
                                          classDesc.className,
                                          classDesc.contractID,
                                          aFileSpec,
                                          aLocation,
                                          aType);
        FlockXPCOMUtils.addCategories(classDesc);
        // callback to allow component specific registration
        classDesc.register(aCompMgr, aFileSpec, aLocation, aType);
      }
      this.printDebug("leaving registerSelf");
    };

    this.unregisterSelf = function genericModule_unregisterSelf(aCompMgr, aLocation, aType) {
      this.printDebug("entering unregisterSelf...");
      var registrar = aCompMgr.QueryInterface(CI.nsIComponentRegistrar);

      for each (let classDesc in this.classes) {
        this.printDebug(" unregistering component '" + classDesc.className + "'");
        // callback to allow classDesc specific unregistration
        classDesc.unregister(aCompMgr, aLocation, aType);
        FlockXPCOMUtils.deleteCategories(classDesc);
        registrar.unregisterFactoryLocation(classDesc.classID, aLocation);
      }
      this.printDebug("leaving unregisterSelf");
    };

    this.getClassObject = function genericModule_getClassObject(aCompMgr, aCID, iid) {
      this.printDebug("getClassObject() called");

      // XXX: Right now, we only support nsIFactory queries, not nsIClassInfo
      // However, we do provide an nsIClassInfo for components that use us
      // so perhaps we *could*. But I'm unsure about the semantics, and
      // there appears to be no users of that feature in the tree. -Manish
      if (!iid.equals(CI.nsIFactory)) {
        this.printDebug("(error) iid was '" + iid + "'");
        throw CR.NS_ERROR_NOT_IMPLEMENTED;
      }

      for each (let classDesc in this.classes) {
        if (aCID.equals(classDesc.classID)) {
          this.printDebug(" ClassID MATCHES component '" + classDesc.className + "'");
          return classDesc.factory;
        }
        this.printDebug(" ClassID does not match component '" + classDesc.className + "'");
      }

      // no component had a matching contract id
      this.printDebug("(ERROR) unable to match cid='" + aCID + "'");
      throw CR.NS_ERROR_FACTORY_NOT_REGISTERED;
    };

    this.QueryInterface = function genericModule_QueryInterface(iid) {
      if (iid.equals(CI.nsIModule) ||
          iid.equals(CI.nsISupports))
      {
        return this;
      }
      throw CR.NS_ERROR_NO_INTERFACE;
    };

    this.canUnload = function genericModule_canUnload(aCompMgr) {
      this.printDebug("canUnload called");
      return true; // return value is ignored for javascript components
    };
  },

/**
 * Creates a generic Javascript XPCOM component with a set of predefined
 * interfaces: nsISupports, nsISupportsPrimitive, nsISupportsCString, nsIClassInfo.
 *
 * @param aName (in)       A descriptive name for the component. Becomes the
 *                         nsIClassInfo property "classDescription"
 * @param aCID (in)        The ClassID for this component.
 * @param aContractID (in) The ContractID for this component.
 * @param aCtor (in)       The constuctor for the object. Usage: return new aCtor();
 * @param aClassFlags (in) Class from nsIClassInfo flags ORed together. To make
 *                         your component a singleton,
 *                         use Components.interfaces.nsIClassInfo.SINGLETON.
 * @param aInterfaceArray (in) The list of interfaces the component supports.
 *                         Do not list any of the predefined interfaces here.
 *                         Example:
 *                         [Components.interfaces.flockILoggingService, Components.interfaces.nsIObserver]
 * @return An object that implements a class factory for the listed interfaces, and
 *         can be QueryInterfaced()'ed for any of the supported interfaces.
 *
 * @see nsIClassInfo
 */
  genericComponent: function FlockXPCOMUtils_genericComponent(aName, aCID, aContractID, aCtor, aClassFlags, aInterfaceArray) {
    // We need to use "this" as it was during component creation, so we
    // take a copy now. Within genericComponent(), references to "this"
    // are from the context of the caller of the defined function.
    var inst = this;

    inst.ctor = aCtor;              // For creating new instances
    inst.classDescription = aName;  // Property of nsIClassInfo interface

    // use FlockXPCOMUtils as the default for printing debugging statements
    inst.showdebug = FlockXPCOMUtils.debug;

    // override printDebug() to make messages slightly more informative
    inst.printDebug = function genericComponent_printDebug(message) {
      if (inst.showdebug) {
        FlockXPCOMUtils.printDebug("FlockXPCOMUtils Cmp: " +inst.classDescription+ ": " + message);
      }
    };

    // Build the list of interfaces supported by the component.
    inst.interfaceArray = [];

    // All components have nsISupports, so add it automatically.
    inst.interfaceArray.push(Components.interfaces.nsISupports);

    // We provide nsIClassInfo for components using this module.
    inst.interfaceArray.push(Components.interfaces.nsIClassInfo);

    inst.predefinedInterfaceCount = inst.interfaceArray.length;

    // Record the interfaces supported by the component.
    for each (let interface in aInterfaceArray) {
      inst.interfaceArray.push(interface);
    }

    // Check that the caller actually gave an interface to use
    if (inst.interfaceArray.length === inst.predefinedInterfaceCount) {
      inst.printDebug("(WARNING) no user specified interfaces");
    }

    // warn if it is obvious something else has been passed instead of a CID
    //   (the raw xpconnect error is obscure enough to warrant a warning)
    if (typeof(aCID) !== "object") {
      inst.printDebug("(WARNING) cid '" + aCID + "' is not an object!");
    }

    // register() is a callback from genericModule.registerSelf()
    inst.register = function genericComponent_register(aCompMgr, aFileSpec, aLocation, aType) {
      inst.printDebug("register called on component '" + inst.classDescription + "'");
      return;  // user overrides 'register()' for component specific registration
    };

    // unregister() is a callback from genericModule.unregisterSelf()
    inst.unregister = function genericComponent_unregister(aCompMgr, aFileSpec, aLocation, aType) {
      inst.printDebug("unregister called on component '" + inst.classDescription + "'");
      return;  // user can override for component specific unregistration
    };

    // Convenience wrapper around FlockXPCOMUtils.addCategories
    inst.addCategories = function genericComponent_addCategories() {
      inst.printDebug("instance manually adding categories");
      var classDesc = FlockXPCOMUtils.classDescForComponent(inst.ctor);
      FlockXPCOMUtils.addCategories(classDesc);
    };

    // Convenience wrapper around FlockXPCOMUtils.deleteCategories
    inst.deleteCategories = function genericComponent_deleteCategories() {
      inst.printDebug("instance manually deleting categories");
      var classDesc = FlockXPCOMUtils.classDescForComponent(inst.ctor);
      FlockXPCOMUtils.deleteCategories(classDesc);
    };

    // nsISupports interface
    inst.QueryInterface = function genericComponent_QueryInterface(iid) {
      inst.printDebug("QueryInterface called");

      // see if component object supports the requested interface
      for each (let interface in inst.interfaceArray) {
        if (iid.equals(interface)) {
          inst.printDebug(" MATCHED interface '" + interface + "'");
          return this;
        }
      }

      if (inst.showdebug) { // expensive but rare check to get better debug message
        for (var name in Components.interfaces) {
          if (iid.equals(Components.interfaces[name])) {
            inst.printDebug(" NS_ERROR_NO_INTERFACE on QI for '" + name + "'");
            throw Components.results.NS_ERROR_NO_INTERFACE;
          }
        }
        inst.printDebug(" NS_ERROR_NO_INTERFACE on QI for '" + iid + "'");
      }
      throw Components.results.NS_ERROR_NO_INTERFACE;
    };

    // nsIClassInfo interface
    inst.getInterfaces = function genericComponent_getInterfaces(aCount) {
      var iids = [];
      for each (let interface in inst.interfaceArray) {
        iids.push(interface);
      }
      aCount.value = iids.length;
      return iids;
    };
    inst.getHelperForLanguage = function genericComponent_getHelperForLanguage(aLanguage) { return null; };
    inst.contractID = aContractID;
    inst.classID = aCID;
    inst.implementationLanguage = Components.interfaces.nsIProgrammingLanguage.JAVASCRIPT;
    // See xpcom/components/nsIClassInfo.idl for more flags.
    inst.flags = aClassFlags;

    // required by nsIFactory so define to prevent warnings
    inst.lockFactory = function genericComponent_lockFactory(lock) {
      inst.printDebug("lockFactory() called");
      return true;
    };

    // nsIFactory function pointed to by module.getClassObject()
    inst.createInstance = function genericComponent_createInstance(outer, iid) {
      inst.printDebug("createInstance called for " + inst.classDescription);

      if (outer) {
        /* FIXME: tell me again why don't we handle aggregation? */
        inst.printDebug("(ERROR) interface does not allow aggregation");
        throw Components.results.NS_ERROR_NO_AGGREGATION;
      }
      inst.printDebug(" creating new instance and querying for interface");
      return (new inst.ctor()).QueryInterface(iid);
    };
  },

  // helper function to add an entry to the category manager
  addCategoryEntry: function FlockXPCOMUtils_addCategoryEntry(category, name, value) {
    this.printDebug("FlockXPCOMUtils addCategory: adding '"
                    + name
                    + "' to category '"
                    + category
                    + "'");
    /* NOTE: different categories use different entries for name and value */
    this.categoryManager.addCategoryEntry(category, name, value, true, true);
  },

  // helper function to remove an entry from the category manager
  deleteCategoryEntry: function FlockXPCOMUtils_deleteCategoryEntry(category, name) {
    this.printDebug("FlockXPCOMUtils deleteCategory: deleting '"
                    + name
                    + "' from category '"
                    + category
                    + "'");
    this.categoryManager.deleteCategoryEntry(category, name, true);
  },

  // helper function add categories to the category manager out of
  // component.prototype._xpcom_categories
  addCategories: function FlockXPCOMUtils_addCategories(classDesc) {
    if (classDesc.categories) {
      for each (let cat in classDesc.categories) {
        let defaultValue = (cat.service ? "service," : "") +
                           classDesc.contractID;
        this.addCategoryEntry(cat.category,
                              cat.entry || classDesc.className,
                              cat.value || defaultValue);
      }
    }
  },

  // helper function delete categories to the category manager out of
  // component.prototype._xpcom_categories
  deleteCategories: function FlockXPCOMUtils_deleteCategories(classDesc) {
    if (classDesc.categories) {
      for each (let cat in classDesc.categories) {
        this.deleteCategoryEntry(cat.category,
                                 cat.entry || classDesc.className);
      }
    }
  },

  classDescForComponent: function FlockXPCOMUtils_classDescForComponent(aComponent) {
    return {
      classID: aComponent.prototype.classID,
      className: aComponent.prototype.classDescription,
      contractID: aComponent.prototype.contractID,
      factory: aComponent.prototype,  // component acts as its own nsIFactory
      categories: aComponent.prototype._xpcom_categories,
      register: aComponent.prototype.register,
      unregister: aComponent.prototype.unregister
    };
  },

  get categoryManager() {
    return CC["@mozilla.org/categorymanager;1"]
           .getService(CI.nsICategoryManager);
  },

  // print a debug message (turn on and off via FlockXPCOMUtils.debug)
  printDebug: function FlockXPCOMUtils_printDebug(message) {
    if (this.debug) {
      debug(message + "\n");
    }
  }
}; // var FlockXPCOMUtils
