/** Asynchronous Functions / Futures. */

/* This module contains helper functions which aid definining and executing
 * asynchronous callbacks in such a way that (a) exceptions don't cause
 * callbacks to be lost, and (b) all paths through the code will invoke a
 * callback (so that unusual conditions don't cause callbacks to be
 * inadvertently skipped).
 *
 * Asynchronous calls kick off the asynchronous computation immediately,
 * returning a Lisp-style "promise" for the result.  See:
 *   http://en.wikipedia.org/wiki/Futures_and_promises
 * Referencing the value of a promise will block for the completion of
 * the asynchronous task.
 *
 * The actual implementation uses Continuation-Passing Style:
 *   http://en.wikipedia.org/wiki/Continuation-passing_style
 * All asynchronous methods take two callbacks, conventionally named
 * onReturn and onError.  Every control-flow path is *guaranteed* to
 * eventually execute one or the other as a tail call.  Normal
 * return invokes onReturn, with the return value (if any) as a parameter;
 * throwing an exception invokes onError, with the exception value
 * (which, in JavaScript, may be null) as a parameter.
 *
 * Typically one will use 'fromGenerator' to transform a JavaScript
 * generator into an asynchronous function in continuation-passing style.
 * Inside the generator, asynchronous functions are invoked using
 * 'asyncCall' resulting in a promise of its future value.  You may
 * immediately (or later) 'yield' a promise to force its value.
 * For example:
 *    let exampleFunc = Async.fromGenerator(function(z) {
 *        let x = yield getX.asyncCall(this);
 *        let y = yield getY.asyncCall(this);
 *        let sum = yield sum.asyncCall(this, x, y, z);
 *        Async.asyncReturn(sum);
 *    };
 * Our exampleFunc has the type 'function(onReturn, onError, z)'.
 * For compatibility with old code, we can convert this to a function
 * which takes the onReturn/onError functions at the end as follows:
 *   let exampleFunc2 = Async.argsAtEnd(1, exampleFunc);
 * This has the type 'function(z, onReturn, onError)'.
 *
 * Our implementation supports "promise pipelining" which allows us to
 * overlap the execution of asynchronous calls.  For example, to
 * overlap the execution of 'getX' and 'getY' in our exampleFunc, we
 * could write:
 *    let exampleFunc = Async.fromGenerator(function(z) {
 *        let xPromise = getX.asyncCall(this);
 *        let yPromise = getY.asyncCall(this);
 *        let x = yield xPromise;
 *        let y = yield yPromise;
 *        let sum = yield sum.asyncCall(this, x, y, z);
 *        Async.asyncReturn(sum);
 *
 * The only difference here is that we performed both asyncCalls before
 * yielding either of the resulting promises (which blocks for a result).
 *
 * See test/js/testAsync.js for more usage examples and comparisons of
 * synchronous and asynchronous programming styles.
 */

/* There are two data types used here:
 *
 *  Asynchronous Functions: functions in Continuation-Passing Style, which
 *   means they take onReturn and onError arguments.  They perform some
 *   computation, and (immediately or at some future time) will invoke
 *   either onReturn (with the 'normal' result of the computation) or
 *   onError (with the exception value).
 *
 *  Promises: represent a value which will be computed in the
 *   future, although it may not be available yet.  Promises have
 *   a 'get' method which is an Asynchronous Function.
 *
 * The Promise datatype is defined in the Promise module,
 * then async calls are layered on top in this file.
 *
 * The general idea is that async.js defines a "new syntax" for calls,
 * while promise.js is "simply" a straightforward class that does not
 * introduce a new convention for calls.
 *
 * As syntactic sugar, we define a 'asyncCall' method on Asynchronous
 * Functions which makes it easy to get a Promise for their result.
 */

/* ------------------------------------------------------------------- */
/* IMPLEMENTATION */

const Promise = imports.promise;

/* Used to mark manual implementations of Asynchronous Functions.
 * Given a function where the first two parameters are an onReturn
 * and an onError callback (and the other arguments are arbitrary
 * strictly-evaluated values).  Adds a 'asyncCall' property to the
 * function, which can be used to invoke the below 'asyncCall' function
 * on it (ie, to create a promise result for an invocation of this
 * Asynchronous Function).
 */
let asyncFunc = Promise._asyncFunc;

/* default onError handler, makes debugging missing callbacks easier. Copied
 * here from Promise because it's also logically part of the "async call" API
 */
const DEFAULT_ONERROR = Promise.DEFAULT_ONERROR;

/* For backwards compatibility, this function reverses the args in an
 * asyncFunc so that the onReturn/onError arguments are at the end.
 * For robustness when given "extra" or "too few" arguments, it also takes
 * the expected # of arguments (not including onReturn/onError).
 * The asyncCall method is copied through untouched, since its calling
 * convention does not change even though the wrapper's arguments change.
 */
let argsAtEnd = function(nArgs, f) {
    let g = function() {
        let params = Array.slice(arguments);
        let [onReturn, onError] = params.splice(nArgs, 2);
        onError = onError || DEFAULT_ONERROR;
        params.unshift(onReturn, onError);
        return f.apply(this, params);
    };
    g.asyncCall = f.asyncCall;
    return g;
};

/* Create a Promise representing the future return value of an
 * Asynchronous Function.  The first argument is the asynchronous function;
 * further arguments are arguments to the asynchronous function.  The
 * 'this' value provided to asyncCall is passed on to the given
 * asynchronous function.  The function is invoked immediately with
 * callbacks which will fill in the promise which is returned to the
 * caller.
 */
let asyncCall = Promise._asyncCall;

/* Create a Promise representing the future construction of an object using
 * the given constructor (passed as 'this') and arguments.  The value of the
 * promise is the fully-constructed object.
 */
let asyncNew = function() {
    let constructor = this;
    let params = Array.slice(arguments, 0); // close arguments

    return Promise.fromConstructor(constructor, params);
};

/* Converts a synchronous function into an Asynchronous Function.
 * The given function may take arbitrary parameters and return values
 * or throw exceptions.  The returned function takes onReturn and onError
 * callbacks in the first positions, and invokes onReturn or onError
 * with the value returned / exception thrown.  (The computation in the
 * function is still performed synchronously; it just uses the Asynchronous
 * Function calling convention so it can be chained with promises, or
 * used in fromGenerator, or invoked via asyncCall, etc.   This
 * is mostly for testing.)
 */
let fromSync = function(f) {
    return asyncFunc(function(onReturn, onError) {
        onError = onError || DEFAULT_ONERROR;
        try {
            let params = Array.slice(arguments, 2);
            onReturn(f.apply(this, params));
        } catch (e) {
            onError(e);
        }
    });
};

/* The coup de grace: turn a synchronous function into an asynchronous one
 * just by adding a few 'yield' statements!  This takes a generator and
 * blocks on the promises yielded by it, returning the values
 * back to the generator.  Thrown exceptions trigger onError, and exceptions
 * returned by onError get transferred into the generator for proper
 * handling there.
 */
let fromGenerator = function(g) {
    return asyncFunc(function(onReturn, onError) {
                         let params = Array.slice(arguments, 2);
                         let promise = Promise.fromGenerator(g,
                                                             this, /* preserve 'this' */
                                                             params);
                         promise.get(onReturn, onError);
                     });
};

/* This function lets you return a value from a generator which has been
 * transformed with fromGenerator. */
let asyncReturn = function(val) {
    Promise.putReturnFromGenerator(val);
};

