//
// FunctionPromise: possibly-asynchronous function constructor.
//
// This is a prototype polyfill for a FunctionPromise object as described in:
//
//    https://bugzilla.mozilla.org/show_bug.cgi?id=854627
//
// Where possible it will arrange for the function body to be parsed/compiled
// off of the main thread, with the function object returned asynchronously
// via a promise.  The fallback implementation processes just falls back to
// the standard synchronous Function() constructor.
// 
// It doesn't (yet) have the following features from the linked proposal:
//
//    * ability to copy to different workers
//    * ability to store in IndexedDB
// 
function FunctionPromise(/* [args1[, args2[, ...argN]],], functionBody) */) {

  var useFallback =
    typeof window === "undefined" ||
    window.FunctionPromise !== FunctionPromise ||
    typeof document === "undefined" ||
    typeof document.createElement === "undefined" ||
    typeof document.head === "undefined" ||
    typeof document.head.appendChild === "undefined" ||
    typeof Blob === "undefined" ||
    typeof URL === "undefined" ||
    typeof URL.createObjectURL === "undefined";

  var args = Array.prototype.slice.call(arguments);

  // For the fallback case, we just use the normal Function constructor.

  if (useFallback) {
    try {
      var fn = Function.apply(null, args);
      return Promise.resolve(fn);
    } catch (err) {
      return Promise.reject(err);
    }
  }

  // If we have all the necessary pieces, we can do this asynchronously
  // by writing a <script> tag into the DOM.

  var funcid = FunctionPromise._nextid++;

  return new Promise(function(resolve, reject) {
    try {
      var funcSrc = [];
      funcSrc.push("window.FunctionPromise._results[" + funcid + "]=");
      funcSrc.push("function(");
      if (args.length > 1) {
        funcSrc.push(args[0]);
        for (var i = 1; i < args.length - 1; i++) {
          funcSrc.push(",");
          funcSrc.push(args[i]);
        }
      }
      funcSrc.push("){");
      funcSrc.push(args[args.length - 1]);
      funcSrc.push("}");
      var dataUrl = URL.createObjectURL(new Blob(funcSrc));
      var scriptTag = document.createElement("script");
      var cleanup = function() {
        URL.revokeObjectURL(dataUrl);
        scriptTag.remove();
        delete window.FunctionPromise._results[funcid];
      }
      scriptTag.onerror = function() {
        reject(new Error("unknown error loading FunctionPromise"))
        cleanup();
      }
      scriptTag.onload = function() {
        if (window.FunctionPromise._results[funcid]) {
          resolve(window.FunctionPromise._results[funcid]);
        } else {
          // No function, something must have gone wrong.
          // Likely a syntax error in the function body string.
          // Fall back to Function() constructor to surface it.
          try {
            Function.apply(null, args);
            reject(new Error("unknown error fulfilling FunctionPromise"));
          } catch (err) {
            reject(err);
          }
        }
        cleanup();
      }
      scriptTag.src = dataUrl;
      document.head.appendChild(scriptTag);
    } catch (err) {
      reject(err);
    }
  });
}

FunctionPromise._nextid = 0;
FunctionPromise._results = {};

if (typeof module !== "undefined" && typeof module.exports !== "undefined") {
  if (typeof Promise === "undefined") {
    Promise = require('es6-promise').Promise;
  }
  module.exports = FunctionPromise;
}