var _ = require("underscore");
var Fiber = require("fibers");

exports.parallelEach = function (collection, callback, context) {
  const errors = [];
  context = context || null;

  const results = Promise.all(_.map(collection, (...args) => {
    async function run() {
      return callback.apply(context, args);
    }

    return run().catch(error => {
      // Collect the errors but do not propagate them so that we can
      // re-throw the first error after all iterations have completed.
      errors.push(error);
    });
  })).await();

  if (errors.length > 0) {
    throw errors[0];
  }

  return results;
};

function disallowedYield() {
  throw new Error("Can't call yield in a noYieldsAllowed block!");
}
// Allow testing Fiber.yield.disallowed.
disallowedYield.disallowed = true;

exports.noYieldsAllowed = function (f, context) {
  var savedYield = Fiber.yield;
  Fiber.yield = disallowedYield;
  try {
    return f.call(context || null);
  } finally {
    Fiber.yield = savedYield;
  }
};

// Borrowed from packages/meteor/dynamics_nodejs.js
  if (!Fiber.current) {
    throw new Error("Meteor code must always run within a Fiber. " +
                    "Try wrapping callbacks that you pass to non-Meteor " +
                    "libraries with Meteor.bindEnvironment.");
  }
};

var nextSlot = 0;
exports.EnvironmentVariable = function (defaultValue) {
  var self = this;
  self.slot = 'slot' + nextSlot++;
  self.defaultValue = defaultValue;
};

Object.assign(exports.EnvironmentVariable.prototype, {
  get() {
    var self = this;
    exports.nodeCodeMustBeInFiber();

    if (!Fiber.current._meteorDynamics) {
      return self.defaultValue;
    }
    if (!_.has(Fiber.current._meteorDynamics, self.slot)) {
      return self.defaultValue;
    }
    return Fiber.current._meteorDynamics[self.slot];
  },

  set(value) {
    exports.nodeCodeMustBeInFiber();

    const fiber = Fiber.current;
    const currentValues = fiber._meteorDynamics || (
      fiber._meteorDynamics = {}
    );

    const saved = _.has(currentValues, this.slot)
      ? currentValues[this.slot]
      : this.defaultValue;

    currentValues[this.slot] = value;

    return () => {
      currentValues[this.slot] = saved;
    };
  },

  withValue(value, func) {
    const reset = this.set(value);
    try {
      return func();
    } finally {
      reset();
    }
  }
});

// This is like Meteor.bindEnvironment.
// Experimentally, we are NOT including onException or _this in this version.
exports.bindEnvironment = function (func) {
  exports.nodeCodeMustBeInFiber();

  var boundValues = _.clone(Fiber.current._meteorDynamics || {});

  return function (...args) {
    var self = this;

    var runWithEnvironment = function () {
      var savedValues = Fiber.current._meteorDynamics;
      try {
        // Need to clone boundValues in case two fibers invoke this
        // function at the same time
        Fiber.current._meteorDynamics = _.clone(boundValues);
        return func.apply(self, args);
      } finally {
        Fiber.current._meteorDynamics = savedValues;
      }
    };

    if (Fiber.current) {
      return runWithEnvironment();
    }
    Fiber(runWithEnvironment).run();
  };
};

// Returns a Promise that supports .resolve(result) and .reject(error).
exports.makeFulfillablePromise = function () {
  var resolve, reject;
  var promise = new Promise(function (res, rej) {
    resolve = res;
    reject = rej;
  });
  promise.resolve = resolve;
  promise.reject = reject;
  return promise;
};
// Used by buildmessage

exports.nodeCodeMustBeInFiber = function () {