'use strict';

var assert = require('assert-plus');
var once = require('once');

module.exports = Chain;

/**
 * Create a new middleware chain
 *
 * @public
 * @class Chain
 * @param {Object} [options] - options
 * @param {Boolean} [options.onceNext=false] - Prevents calling next multiple
 *  times
 * @param {Boolean} [options.strictNext=false] - Throws error when next() is
 *  called more than once, enables onceNext option
 * @example
 * var chain = new Chain();
 * chain.add(function (req, res, next) { next(); })
 * // chain.add(function (req, res, next) { next(new Error('Foo')); })
 * // chain.add(function (req, res, next) { next(false); })
 *
 * http.createServer((req, res) => {
 *    chain.run(req, res, function done(err) {
 *       res.end(err ? err.message : 'hello world');
 *    });
 * })
 */
function Chain(options) {
    assert.optionalObject(options, 'options');
    options = options || {};
    assert.optionalBool(options.onceNext, 'options.onceNext');
    assert.optionalBool(options.strictNext, 'options.strictNext');

    this.onceNext = !!options.onceNext;
    this.strictNext = !!options.strictNext;

    // strictNext next enforces onceNext
    if (this.strictNext) {
        this.onceNext = true;
    }

    this._stack = [];
    this._once = this.strictNext === false ? once : once.strict;
}

/**
 * Public methods.
 * @private
 */

/**
 * Get handlers of a chain instance
 *
 * @memberof Chain
 * @instance
 * @returns {Function[]} handlers
 */
Chain.prototype.getHandlers = function getHandlers() {
    return this._stack;
};

/**
 * Utilize the given middleware `handler`
 *
 * @public
 * @memberof Chain
 * @instance
 * @param {Function} handler - handler
 * @returns {undefined} no return value
 */
Chain.prototype.add = function add(handler) {
    // _name is assigned in the server and router
    handler._name = handler._name || handler.name;

    // add the middleware
    this._stack.push(handler);
};

/**
 * Returns the number of handlers
 *
 * @public
 * @memberof Chain
 * @instance
 * @returns {Number} number of handlers in the stack
 */
Chain.prototype.count = function count() {
    return this._stack.length;
};

/**
 * Handle server requests, punting them down
 * the middleware stack.
 *
 * @public
 * @memberof Chain
 * @instance
 * @param {Request} req - request
 * @param {Response} res - response
 * @param {Function} done - final handler
 * @returns {undefined} no return value
 */
Chain.prototype.run = function run(req, res, done) {
    var self = this;
    var index = 0;

    function next(err) {
        // next callback
        var handler = self._stack[index++];

        // all done or request closed
        if (!handler || req.closed()) {
            process.nextTick(function nextTick() {
                return done(err, req, res);
            });
            return;
        }

        // call the handler
        call(handler, err, req, res, self.onceNext ? self._once(next) : next);
    }

    next();
    return;
};

/**
 * Helper functions
 * @private
 */

/**
 * Invoke a handler.
 *
 * @private
 * @param {Function} handler - handler function
 * @param {Error|false|*} err - error, abort when true value or false
 * @param {Request} req - request
 * @param {Response} res - response
 * @param {Function} _next - next handler
 * @returns {undefined} no return value
 */
function call(handler, err, req, res, _next) {
    var arity = handler.length;
    var error = err;
    var hasError = err === false || Boolean(err);

    // Meassure handler timings
    // _name is assigned in the server and router
    req._currentHandler = handler._name;
    req.startHandlerTimer(handler._name);

    function next(nextErr) {
        req.endHandlerTimer(handler._name);
        _next(nextErr, req, res);
    }

    if (hasError && arity === 4) {
        // error-handling middleware
        handler(err, req, res, next);
        return;
    } else if (!hasError && arity < 4) {
        // request-handling middleware
        process.nextTick(function nextTick() {
            handler(req, res, next);
        });
        return;
    }

    // continue
    next(error, req, res);
    return;
}
