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

(function (factory) { // Module boilerplate
  if (this.module && module.id.indexOf("worker") >= 0) { // require
    const { Cc, Ci, ChromeWorker } = require("chrome");
    factory.call(this, require, exports, module, { Cc, Ci }, ChromeWorker);
  } else { // Cu.import
      const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
      const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
      this.isWorker = false;
      this.Promise = Cu.import("resource://gre/modules/Promise.jsm", {}).Promise;
      this.console = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).console;
      factory.call(this, devtools.require, this, { exports: this }, { Cc, Ci }, ChromeWorker);
      this.EXPORTED_SYMBOLS = ["DevToolsWorker"];
  }
}).call(this, function (require, exports, module, { Ci, Cc }, ChromeWorker ) {

let MESSAGE_COUNTER = 0;

/**
 * Creates a wrapper around a ChromeWorker, providing easy
 * communication to offload demanding tasks. The corresponding URL
 * must implement the interface provided by `devtools/shared/worker-helper`.
 *
 * @see `./browser/devtools/shared/widgets/GraphsWorker.js`
 *
 * @param {string} url
 *        The URL of the worker.
 */
function DevToolsWorker (url) {
  this._worker = new ChromeWorker(url);
}
exports.DevToolsWorker = DevToolsWorker;

/**
 * Performs the given task in a chrome worker, passing in data.
 * Returns a promise that resolves when the task is completed, resulting in
 * the return value of the task.
 *
 * @param {string} task
 *        The name of the task to execute in the worker.
 * @param {any} data
 *        Data to be passed into the task implemented by the worker.
 * @return {Promise}
 */
DevToolsWorker.prototype.performTask = function DevToolsWorkerPerformTask (task, data) {
  if (this._destroyed) {
    return Promise.reject("Cannot call performTask on a destroyed DevToolsWorker");
  }
  let worker = this._worker;
  let id = ++MESSAGE_COUNTER;
  worker.postMessage({ task, id, data });

  return new Promise(function (resolve, reject) {
    worker.addEventListener("message", function listener({ data }) {
      if (data.id !== id) {
        return;
      }
      worker.removeEventListener("message", listener);
      if (data.error) {
        reject(data.error);
      } else {
        resolve(data.response);
      }
    });
  });
}

/**
 * Terminates the underlying worker. Use when no longer needing the worker.
 */
DevToolsWorker.prototype.destroy = function DevToolsWorkerDestroy () {
  this._worker.terminate();
  this._worker = null;
  this._destroyed = true;
};

/**
 * Takes a function and returns a Worker-wrapped version of the same function.
 * Returns a promise upon resolution.
 * @see `./browser/devtools/shared/test/browser_devtools-worker-03.js
 *
 * * * * ! ! ! This should only be used for tests or A/B testing performance ! ! ! * * * * * *
 *
 * The original function must:
 *
 * Be a pure function, that is, not use any variables not declared within the
 * function, or its arguments.
 *
 * Return a value or a promise.
 *
 * Note any state change in the worker will not affect the callee's context.
 *
 * @param {function} fn
 * @return {function}
 */
function workerify (fn) {
  console.warn(`\`workerify\` should only be used in tests or measuring performance.
  This creates an object URL on the browser window, and should not be used in production.`)
  // Fetch via window/utils here as we don't want to include
  // this module normally.
  let { getMostRecentBrowserWindow } = require("sdk/window/utils");
  let { URL, Blob } = getMostRecentBrowserWindow();
  let stringifiedFn = createWorkerString(fn);
  let blob = new Blob([stringifiedFn]);
  let url = URL.createObjectURL(blob);
  let worker = new DevToolsWorker(url);

  let wrapperFn = data => worker.performTask("workerifiedTask", data);

  wrapperFn.destroy = function () {
    URL.revokeObjectURL(url);
    worker.destroy();
  };

  return wrapperFn;
}
exports.workerify = workerify;

/**
 * Takes a function, and stringifies it, attaching the worker-helper.js
 * boilerplate hooks.
 */
function createWorkerString (fn) {
  return `importScripts("resource://gre/modules/workers/require.js");
    const { createTask } = require("resource:///modules/devtools/shared/worker-helper");
    createTask(self, "workerifiedTask", ${fn.toString()});
  `;
}

});
