/* 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";

// A CommonJS module loader that is designed to run inside a worker debugger.
// We can't simply use the SDK module loader, because it relies heavily on
// Components, which isn't available in workers.
//
// In principle, the standard instance of the worker loader should provide the
// same built-in modules as its devtools counterpart, so that both loaders are
// interchangable on the main thread, making them easier to test.
//
// On the worker thread, some of these modules, in particular those that rely on
// the use of Components and for which the worker debugger doesn't provide an
// alternative API, will be replaced by vacuous objects. Consequently, they can
// still be required, but any attempts to use them will lead to an exception.

this.EXPORTED_SYMBOLS = ["WorkerDebuggerLoader", "worker"];

// Some notes on module ids and URLs:
//
// An id is either a relative id or an absolute id. An id is relative if and
// only if it starts with a dot. An absolute id is a normalized id if and only if
// it contains no redundant components. Every normalized id is a URL.
//
// A URL is either an absolute URL or a relative URL. A URL is absolute if and
// only if it starts with a scheme name followed by a colon and 2 slashes.

// Resolve the given relative id to an absolute id.
function resolveId(id, baseId) {
  return baseId + "/../" + id;
};

// Convert the given absolute id to a normalized id.
function normalizeId(id) {
  // An id consists of an optional root and a path. A root consists of either
  // a scheme name followed by 2 or 3 slashes, or a single slash. Slashes in the
  // root are not used as separators, so only normalize the path.
  let [_, root, path] = id.match(/^(\w+:\/\/\/?|\/)?(.*)/);

  let stack = [];
  path.split("/").forEach(function (component) {
    switch (component) {
    case "":
    case ".":
      break;
    case "..":
      if (stack.length === 0) {
        if (root !== undefined) {
          throw new Error("can't convert absolute id " + id + " to " +
                          "normalized id because it's going past root!");
        } else {
          stack.push("..");
        }
      } else {
        if (stack[stack.length] == "..") {
          stack.push("..");
        } else {
          stack.pop();
        }
      }
      break;
    default:
      stack.push(component);
      break;
    }
  });

  return (root ? root : "") + stack.join("/");
}

// Create a module object with the given id.
function createModule(id) {
  return Object.create(null, {
    // CommonJS specifies the id property to be non-configurable and
    // non-writable.
    id: {
      configurable: false,
      enumerable: true,
      value: id,
      writable: false
    },

    // CommonJS does not specify an exports property, so follow the NodeJS
    // convention, which is to make it non-configurable and writable.
    exports: {
      configurable: false,
      enumerable: true,
      value: Object.create(null),
      writable: true
    }
  });
};

// Create a CommonJS loader with the following options:
// - createSandbox:
//     A function that will be used to create sandboxes. It takes the name and
//     prototype of the sandbox to be created, and should return the newly
//     created sandbox as result. This option is mandatory.
// - globals:
//     A map of built-in globals that will be exposed to every module. Defaults
//     to the empty map.
// - loadInSandbox:
//     A function that will be used to load scripts in sandboxes. It takes the
//     URL from which and the sandbox in which the script is to be loaded, and
//     should not return a result. This option is mandatory.
// - modules:
//     A map of built-in modules that will be added to the module cache.
//     Defaults to the empty map.
// - paths:
//     A map of paths to base URLs that will be used to resolve relative URLs to
//     absolute URLS. Defaults to the empty map.
// - resolve:
//     A function that will be used to resolve relative ids to absolute ids. It
//     takes the relative id of the module to be required and the normalized id
//     of the requiring module as arguments, and should return the absolute id
//     of the module to be required as result. Defaults to resolveId above.
function WorkerDebuggerLoader(options) {
  // Resolve the given relative URL to an absolute URL.
  function resolveURL(url) {
    let found = false;
    for (let [path, baseURL] of paths) {
      if (url.startsWith(path)) {
        found = true;
        url = url.replace(path, baseURL);
        break;
      }
    }
    if (!found) {
      throw new Error("can't resolve relative URL " + url + " to absolute " +
                      "URL!");
    }

    // If the url has no extension, use ".js" by default.
    return url.endsWith(".js") ? url : url + ".js";
  }

  // Load the given module with the given url.
  function loadModule(module, url) {
    // CommonJS specifies 3 free variables named require, exports, and module,
    // that must be exposed to every module, so define these as properties on
    // the sandbox prototype. Additional built-in globals are exposed by making
    // the map of built-in globals the prototype of the sandbox prototype.
    let prototype = Object.create(globals);
    prototype.Components = {};
    prototype.require = createRequire(module);
    prototype.exports = module.exports;
    prototype.module = module;

    let sandbox = createSandbox(url, prototype);
    try {
      loadInSandbox(url, sandbox);
    } catch (error) {
      if (String(error) === "Error opening input stream (invalid filename?)") {
        throw new Error("can't load module " + module.id + " with url " + url +
                        "!");
      }
      throw error;
    }

    // The value of exports may have been changed by the module script, so only
    // freeze it if it is still an object.
    if (typeof module.exports === "object" && module.exports !== null) {
      Object.freeze(module.exports);
    }
  };

  // Create a require function for the given requirer. If no requirer is given,
  // create a require function for top-level modules instead.
  function createRequire(requirer) {
    return function require(id) {
      // Make sure an id was passed.
      if (id === undefined) {
        throw new Error("can't require module without id!");
      }

      // Built-in modules are cached by id rather than URL, so try to find the
      // module to be required by id first.
      let module = modules[id];
      if (module === undefined) {
        // Failed to find the module to be required by id, so convert the id to
        // a URL and try again.

        // If the id is relative, resolve it to an absolute id.
        if (id.startsWith(".")) {
          if (requirer === undefined) {
            throw new Error("can't require top-level module with relative id " +
                            id + "!");
          }
          id = resolve(id, requirer.id);
        }

        // Convert the absolute id to a normalized id.
        id = normalizeId(id);

        // Convert the normalized id to a URL.
        let url = id;

        // If the URL is relative, resolve it to an absolute URL.
        if (url.match(/^\w+:\/\//) === null) {
          url = resolveURL(id);
        }

        // Try to find the module to be required by URL.
        module = modules[url];
        if (module === undefined) {
          // Failed to find the module to be required in the cache, so create
          // a new module, load it with the given URL, and add it to the cache.

          // Add modules to the cache early so that any recursive calls to
          // require for the same module will return the partially-loaded module
          // from the cache instead of triggering a new load.
          module = modules[url] = createModule(id);

          try {
            loadModule(module, url);
          } catch (error) {
            // If the module failed to load, remove it from the cache so that
            // subsequent calls to require for the same module will trigger a
            // new load instead of returning a partially-loaded module from
            // the cache.
            delete modules[url];
            throw error;
          }

          Object.freeze(module);
        }
      }

      return module.exports;
    };
  }

  let createSandbox = options.createSandbox;
  let globals = options.globals || Object.create(null);
  let loadInSandbox = options.loadInSandbox;

  // Create the module cache by converting each entry in the map of built-in
  // modules to a module object with its exports property set to a frozen
  // version of the original entry.
  let modules = options.modules || {};
  for (let id in modules) {
    let module = createModule(id);
    module.exports = Object.freeze(modules[id]);
    modules[id] = module;
  }

  // Convert the map of paths to baseURLs into an array for use by resolveURL.
  // The array is sorted from longest to shortest path so the longest path will
  // always be the first to be found.
  let paths = options.paths || {};
  paths = Object.keys(paths)
                .sort((a, b) => b.length - a.length)
                .map(path => [path, paths[path]]);

  let resolve = options.resolve || resolveId;

  this.require = createRequire();
}

this.WorkerDebuggerLoader = WorkerDebuggerLoader;

const chrome = { CC: undefined, Cc: undefined, ChromeWorker: undefined,
                 Cm: undefined, Ci: undefined, Cu: undefined,
                 Cr: undefined, components: undefined };

// The default instance of the worker debugger loader is defined differently
// depending on whether it is loaded from the main thread or a worker thread.
if (typeof Components === "object") {
  const { Constructor: CC, classes: Cc, manager: Cm, interfaces: Ci,
          results: Cr, utils: Cu } = Components;

  const principal = CC('@mozilla.org/systemprincipal;1', 'nsIPrincipal')();

  // Create a sandbox with the given name and prototype.
  const createSandbox = function (name, prototype) {
    return Cu.Sandbox(principal, {
      invisibleToDebugger: true,
      sandboxName: name,
      sandboxPrototype: prototype,
      wantComponents: false,
      wantXrays: false
    });
  };

  const loadSubScript = Cc['@mozilla.org/moz/jssubscript-loader;1'].
                        getService(Ci.mozIJSSubScriptLoader).loadSubScript;

  // Load a script from the given URL in the given sandbox.
  const loadInSandbox = function (url, sandbox) {
    loadSubScript(url, sandbox, "UTF-8");
  };

  // Define the Debugger object in a sandbox to ensure that the this passed to
  // addDebuggerToGlobal is a global.
  let sandbox = Cu.Sandbox(principal, {});
  Cu.evalInSandbox(
    "Components.utils.import('resource://gre/modules/jsdebugger.jsm');" +
    "addDebuggerToGlobal(this);",
    sandbox
  );
  const Debugger = sandbox.Debugger;

  const Timer = Cu.import("resource://gre/modules/Timer.jsm", {});
  const xpcInspector = Cc["@mozilla.org/jsinspector;1"].
                       getService(Ci.nsIJSInspector);

  this.worker = new WorkerDebuggerLoader({
    createSandbox: createSandbox,
    globals: {
      "isWorker": true,
      "reportError": Cu.reportError,
    },
    loadInSandbox: loadInSandbox,
    modules: {
      "Services": {},
      "chrome": chrome,
      "promise": Promise,
      "Debugger": Debugger,
      "xpcInspector": xpcInspector,
      "Timer": Object.create(Timer)
    },
    paths: {
      "": "resource://gre/modules/commonjs/",
      "devtools": "resource:///modules/devtools",
      "devtools/server": "resource://gre/modules/devtools/server",
      "devtools/toolkit": "resource://gre/modules/devtools",
      "source-map": "resource://gre/modules/devtools/source-map",
      "xpcshell-test": "resource://test",
    }
  });
}
