'use strict';

/**
 * @method sequence
 * @summary Resolves a dynamic sequence of $[mixed values].
 * @description
 * **Alternative Syntax:**
 * `sequence(source, {dest, limit, track})` &#8658; `Promise`
 *
 * Acquires $[mixed values] from the source function, one at a time, and resolves them,
 * till either no more values left in the sequence or an error/reject occurs.
 * It supports both [linked and detached sequencing](../concept/sequencing.md).
 *
 * <img src="../images/sequence.png" width="561px" height="193px" alt="sequence">
 *
 * @param {Function|generator} source
 * Expected to return the next $[mixed value] to be resolved. Returning or resolving
 * with `undefined` ends the sequence, and the method resolves.
 *
 * Parameters:
 *  - `index` = current request index in the sequence
 *  - `data` = resolved data from the previous call (`undefined` when `index=0`)
 *  - `delay` = number of milliseconds since the last call (`undefined` when `index=0`)
 *
 * The function is called with the same `this` context as the calling method.
 *
 * If the function throws an error or returns a rejected promise, the sequence terminates,
 * and the method rejects with object `{index, error, source}`:
 *  - `index` = index of the request that failed
 *  - `error` = the error thrown or the rejection reason
 *  - `source` = resolved `data` that was passed into the function
 *
 * Passing in anything other than a function will throw `Invalid sequence source.`
 *
 * @param {Function|generator} [dest]
 * Optional destination function (or generator), to receive resolved data for each index,
 * process it and respond as required.
 *
 * Parameters:
 *  - `index` = index of the resolved data in the sequence
 *  - `data` = the data resolved
 *  - `delay` = number of milliseconds since the last call (`undefined` when `index=0`)
 *
 * The function is called with the same `this` context as the calling method.
 *
 * It can optionally return a promise object, if data processing is done asynchronously.
 * If a promise is returned, the method will not request another value from the `source` function,
 * until the promise has been resolved.
 *
 * If the function throws an error or returns a rejected promise, the sequence terminates,
 * and the method rejects with object `{index, error, dest}`:
 * - `index` = index of the data that was processed
 * - `error` = the error thrown or the rejection reason
 * - `dest` = resolved data that was passed into the function
 *
 * @param {Number} [limit=0]
 * Limits the maximum size of the sequence. If the value is greater than 0, the method will
 * successfully resolve once the specified limit has been reached.
 *
 * When `limit` isn't specified (default), the sequence is unlimited, and it will continue
 * till one of the following occurs:
 *  - `source` either returns or resolves with `undefined`
 *  - either `source` or `dest` functions throw an error or return a rejected promise
 *
 * @param {Boolean} [track=false]
 * Changes the type of data to be resolved by this method. By default, it is `false`
 * (see the return result). When set to be `true`, the method tracks/collects all resolved data
 * into an array internally, and resolves with that array once the method has finished successfully.
 *
 * It must be used with caution, as to the size of the sequence, because accumulating data for
 * a very large sequence can result in consuming too much memory.
 *
 * @returns {Promise}
 * When successful, the resolved data depends on parameter `track`. When `track` is `false`
 * (default), the method resolves with object `{total, duration}`:
 *  - `total` = number of values resolved by the sequence
 *  - `duration` = number of milliseconds consumed by the method
 *
 * When `track` is `true`, the method resolves with an array of all the data that has been resolved,
 * the same way that the standard `promise.all` resolves. In addition, the array comes extended with
 * read-only property `duration` - number of milliseconds consumed by the method.
 *
 * When the method fails, the reject result depends on which function caused the failure - `source`
 * or `dest`. See the two parameters for the rejection details.
 */
function sequence(source, dest, limit, track, config) {

    if (typeof source !== 'function') {
        throw new TypeError("Invalid sequence source.");
    }

    var $p = config.promise, $utils = config.utils;
    
    limit = (limit > 0) ? parseInt(limit) : 0;
    source = $utils.wrap(source);
    dest = $utils.wrap(dest);

    var self = this, data, srcTime, destTime, result = [], start = Date.now();

    return $p(function (resolve, reject) {

        function loop(idx) {
            var srcNow = Date.now(),
                srcDelay = idx ? (srcNow - srcTime) : undefined;
            srcTime = srcNow;
            $utils.resolve.call(self, source, [idx, data, srcDelay], function (value, delayed) {
                data = value;
                if (data === undefined) {
                    finish();
                } else {
                    if (track) {
                        result.push(data);
                    }
                    if (dest) {
                        var destResult, destNow = Date.now(),
                            destDelay = idx ? (destNow - destTime) : undefined;
                        destTime = destNow;
                        try {
                            destResult = dest.call(self, idx, data, destDelay);
                        } catch (e) {
                            reject({
                                index: idx,
                                error: e,
                                dest: data
                            });
                            return;
                        }
                        if ($utils.isPromise(destResult)) {
                            destResult
                                .then(function () {
                                    next(true);
                                })
                                .catch(function (error) {
                                    reject({
                                        index: idx,
                                        error: error,
                                        dest: data
                                    });
                                });
                        } else {
                            next(delayed);
                        }
                    } else {
                        next(delayed);
                    }
                }
            }, function (reason) {
                reject({
                    index: idx,
                    error: reason,
                    source: data
                });
            });

            function next(delayed) {
                if (limit === ++idx) {
                    finish();
                } else {
                    if (delayed) {
                        loop(idx);
                    } else {
                        $p.resolve()
                            .then(function () {
                                loop(idx);
                            });
                    }
                }
            }

            function finish() {
                var length = Date.now() - start;
                if (track) {
                    $utils.extend(result, 'duration', length);
                } else {
                    result = {
                        total: idx,
                        duration: length
                    }
                }
                resolve(result);
            }
        }

        loop(0);
    });
}

module.exports = function (config) {
    return function (source, dest, limit, track) {
        if (dest && typeof dest === 'object') {
            return sequence.call(this, source, dest.dest, dest.limit, dest.track, config);
        } else {
            return sequence.call(this, source, dest, limit, track, config);
        }
    };
};
