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

"use strict";

/**
 * This module defines custom globals injected in all our modules and also
 * pseudo modules that aren't separate files but just dynamically set values.
 *
 * As it does so, the module itself doesn't have access to these globals,
 * nor the pseudo modules. Be careful to avoid loading any other js module as
 * they would also miss them.
 */

const { Cu, CC, Cc, Ci } = require("chrome");
const { Loader } = Cu.import("resource://gre/modules/commonjs/toolkit/loader.js", {});
const promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
const jsmScope = Cu.import("resource://gre/modules/Services.jsm", {});
const { Services } = jsmScope;
// Steal various globals only available in JSM scope (and not Sandbox one)
const { PromiseDebugging, ChromeUtils, ThreadSafeChromeUtils, HeapSnapshot,
        atob, btoa, Iterator } = jsmScope;
const { URL } = Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")(),
                           {wantGlobalProperties: ["URL"]});

/**
 * Defines a getter on a specified object that will be created upon first use.
 *
 * @param aObject
 *        The object to define the lazy getter on.
 * @param aName
 *        The name of the getter to define on aObject.
 * @param aLambda
 *        A function that returns what the getter should return.  This will
 *        only ever be called once.
 */
function defineLazyGetter(aObject, aName, aLambda)
{
  Object.defineProperty(aObject, aName, {
    get: function () {
      // Redefine this accessor property as a data property.
      // Delete it first, to rule out "too much recursion" in case aObject is
      // a proxy whose defineProperty handler might unwittingly trigger this
      // getter again.
      delete aObject[aName];
      let value = aLambda.apply(aObject);
      Object.defineProperty(aObject, aName, {
        value,
        writable: true,
        configurable: true,
        enumerable: true
      });
      return value;
    },
    configurable: true,
    enumerable: true
  });
}

/**
 * Defines a getter on a specified object for a service.  The service will not
 * be obtained until first use.
 *
 * @param aObject
 *        The object to define the lazy getter on.
 * @param aName
 *        The name of the getter to define on aObject for the service.
 * @param aContract
 *        The contract used to obtain the service.
 * @param aInterfaceName
 *        The name of the interface to query the service to.
 */
function defineLazyServiceGetter(aObject, aName, aContract, aInterfaceName)
{
  defineLazyGetter(aObject, aName, function XPCU_serviceLambda() {
    return Cc[aContract].getService(Ci[aInterfaceName]);
  });
}

/**
 * Defines a getter on a specified object for a module.  The module will not
 * be imported until first use. The getter allows to execute setup and
 * teardown code (e.g.  to register/unregister to services) and accepts
 * a proxy object which acts on behalf of the module until it is imported.
 *
 * @param aObject
 *        The object to define the lazy getter on.
 * @param aName
 *        The name of the getter to define on aObject for the module.
 * @param aResource
 *        The URL used to obtain the module.
 * @param aSymbol
 *        The name of the symbol exported by the module.
 *        This parameter is optional and defaults to aName.
 * @param aPreLambda
 *        A function that is executed when the proxy is set up.
 *        This will only ever be called once.
 * @param aPostLambda
 *        A function that is executed when the module has been imported to
 *        run optional teardown procedures on the proxy object.
 *        This will only ever be called once.
 * @param aProxy
 *        An object which acts on behalf of the module to be imported until
 *        the module has been imported.
 */
function defineLazyModuleGetter(aObject, aName, aResource, aSymbol,
                                aPreLambda, aPostLambda, aProxy)
{
  let proxy = aProxy || {};

  if (typeof (aPreLambda) === "function") {
    aPreLambda.apply(proxy);
  }

  defineLazyGetter(aObject, aName, function XPCU_moduleLambda() {
    var temp = {};
    try {
      Cu.import(aResource, temp);

      if (typeof (aPostLambda) === "function") {
        aPostLambda.apply(proxy);
      }
    } catch (ex) {
      Cu.reportError("Failed to load module " + aResource + ".");
      throw ex;
    }
    return temp[aSymbol || aName];
  });
}

/**
 * Define a getter property on the given object that requires the given
 * module. This enables delaying importing modules until the module is
 * actually used.
 *
 * @param Object obj
 *    The object to define the property on.
 * @param String property
 *    The property name.
 * @param String module
 *    The module path.
 * @param Boolean destructure
 *    Pass true if the property name is a member of the module's exports.
 */
function lazyRequireGetter(obj, property, module, destructure) {
  Object.defineProperty(obj, property, {
    get: () => {
      // Redefine this accessor property as a data property.
      // Delete it first, to rule out "too much recursion" in case obj is
      // a proxy whose defineProperty handler might unwittingly trigger this
      // getter again.
      delete obj[property];
      let value = destructure
        ? require(module)[property]
        : require(module || property);
      Object.defineProperty(obj, property, {
        value,
        writable: true,
        configurable: true,
        enumerable: true
      });
      return value;
    },
    configurable: true,
    enumerable: true
  });
}

// List of pseudo modules exposed to all devtools modules.
exports.modules = {
  "Services": Object.create(Services),
  "toolkit/loader": Loader,
  promise,
  PromiseDebugging,
  ChromeUtils,
  ThreadSafeChromeUtils,
  HeapSnapshot,
};

defineLazyGetter(exports.modules, "Debugger", () => {
  // addDebuggerToGlobal only allows adding the Debugger object to a global. The
  // this object is not guaranteed to be a global (in particular on B2G, due to
  // compartment sharing), so add the Debugger object to a sandbox instead.
  let sandbox = Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")());
  Cu.evalInSandbox(
    "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
    "addDebuggerToGlobal(this);",
    sandbox
  );
  return sandbox.Debugger;
});

defineLazyGetter(exports.modules, "Timer", () => {
  let {setTimeout, clearTimeout} = Cu.import("resource://gre/modules/Timer.jsm", {});
  // Do not return Cu.import result, as SDK loader would freeze Timer.jsm globals...
  return {
    setTimeout,
    clearTimeout
  };
});

defineLazyGetter(exports.modules, "xpcInspector", () => {
  return Cc["@mozilla.org/jsinspector;1"].getService(Ci.nsIJSInspector);
});

defineLazyGetter(exports.modules, "FileReader", () => {
  let sandbox
    = Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")(),
                 {wantGlobalProperties: ["FileReader"]});
  return sandbox.FileReader;
});

// List of all custom globals exposed to devtools modules.
// Changes here should be mirrored to devtools/.eslintrc.
const globals = exports.globals = {
  isWorker: false,
  reportError: Cu.reportError,
  atob: atob,
  btoa: btoa,
  URL,
  loader: {
    lazyGetter: defineLazyGetter,
    lazyImporter: defineLazyModuleGetter,
    lazyServiceGetter: defineLazyServiceGetter,
    lazyRequireGetter: lazyRequireGetter,
    id: null // Defined by Loader.jsm
  },

  // Let new XMLHttpRequest do the right thing.
  XMLHttpRequest: function () {
    return Cc["@mozilla.org/xmlextras/xmlhttprequest;1"]
           .createInstance(Ci.nsIXMLHttpRequest);
  },

  Node: Ci.nsIDOMNode,
  Element: Ci.nsIDOMElement,
  DocumentFragment: Ci.nsIDOMDocumentFragment,

  // Make sure `define` function exists.  This allows defining some modules
  // in AMD format while retaining CommonJS compatibility through this hook.
  // JSON Viewer needs modules in AMD format, as it currently uses RequireJS
  // from a content document and can't access our usual loaders.  So, any
  // modules shared with the JSON Viewer should include a define wrapper:
  //
  //   // Make this available to both AMD and CJS environments
  //   define(function(require, exports, module) {
  //     ... code ...
  //   });
  //
  // Bug 1248830 will work out a better plan here for our content module
  // loading needs, especially as we head towards devtools.html.
  define(factory) {
    factory(this.require, this.exports, this.module);
  },
};

// Lazily define a few things so that the corresponding jsms are only loaded
// when used.
defineLazyGetter(globals, "console", () => {
  return Cu.import("resource://gre/modules/Console.jsm", {}).console;
});
defineLazyGetter(globals, "clearTimeout", () => {
  return Cu.import("resource://gre/modules/Timer.jsm", {}).clearTimeout;
});
defineLazyGetter(globals, "setTimeout", () => {
  return Cu.import("resource://gre/modules/Timer.jsm", {}).setTimeout;
});
defineLazyGetter(globals, "clearInterval", () => {
  return Cu.import("resource://gre/modules/Timer.jsm", {}).clearInterval;
});
defineLazyGetter(globals, "setInterval", () => {
  return Cu.import("resource://gre/modules/Timer.jsm", {}).setInterval;
});
defineLazyGetter(globals, "CSSRule", () => Ci.nsIDOMCSSRule);
defineLazyGetter(globals, "DOMParser", () => {
  return CC("@mozilla.org/xmlextras/domparser;1", "nsIDOMParser");
});
defineLazyGetter(globals, "CSS", () => {
  let sandbox
    = Cu.Sandbox(CC("@mozilla.org/systemprincipal;1", "nsIPrincipal")(),
                 {wantGlobalProperties: ["CSS"]});
  return sandbox.CSS;
});
defineLazyGetter(globals, "WebSocket", () => {
  return Services.appShell.hiddenDOMWindow.WebSocket;
});
lazyRequireGetter(globals, "indexedDB", "sdk/indexed-db", true);
