Source: amoeba.js

var Chain = require('chaining-tool');
var util = require('util');
var EventEmitter = require('events').EventEmitter;

/**
 * Creates a new Amoeba
 * @class
 * @author Maksym Pomazan
 * @version 0.2.0
 * @param {Amoeba} parent The parent amoeba
 * @param {String} p      Path for current amoeba
 */
Amoeba = function(parent, p) {
    this._parent = parent || null;
    this._path = p || null;
    this._vacuols = {};
    this._handlers = {};
};

util.inherits(Amoeba, EventEmitter);

/**
 * Method return root amoeba
 *
 * @return {Amoeba} The root amoeba
 */
Amoeba.prototype.root = function() {
    if (this._parent) {
        return this._parent.root();
    } else {
        return this;
    }
};

/**
 * Method return new Amoeba accroding path
 *
 * @example
 * var amoeba = new Amoeba();
 * amoeba.path("chat").on("message", function(){});
 * @param  {String} p Path
 * @return {Amoeba}   new created amoeba
 */
Amoeba.prototype.path = function(p) {
    return new Amoeba(this.root(), p);
};

/**
 * Method synonim for path method
 */
Amoeba.prototype.vacuole = Amoeba.prototype.path;

/**
 * Route amoeba path to Object
 *
 * @param {Object} Object for route
 * @return {Amoeba} self for chain execution
 */
Amoeba.prototype.as = function() {
    if (this._path) {
        this.root()._vacuols[this._path] = arguments[0];
        if (arguments[0].init) {
            arguments[0].init(this, arguments[1]);
        } else {
            if (arguments[1]) {
                arguments[1](null, {
                    success: true
                });
            }
        }
        return this;
    } else {
        throw new Error("Path not found");
    }
};

/**
 * Add event listener
 *
 * @param {String} event Event to listen. Can be mask.
 * @param {Function} callback Callback function
 * @return {Amoeba} self for chain execution
 */
Amoeba.prototype.on = function() {
    if (this._parent) {
        Array.prototype.splice.call(arguments, 1, 0, this._path);
        this.on.apply(this.root(), arguments);
    } else {
        EventEmitter.prototype.on.call(this, arguments[0] + ":" + arguments[1], arguments[2]);
    }
    return this;
};

/**
 * Emit event
 *
 * @param {String} event Event to emit
 * @param {Object} data Event data
 * @return {Amoeba} self for chain execution
 */
Amoeba.prototype.emit = function() {
    var delimiter = ":";
    var path_delimiter = ".";
    if (this._parent) {
        this.root().emit(arguments[0], this._path, arguments[1]);
    } else {
        var event = arguments[0];
        var path = arguments[1];
        var data = arguments[2];

        EventEmitter.prototype.emit.call(this, event + delimiter + path, data, event, path);
        EventEmitter.prototype.emit.call(this, "*" + delimiter + path, data, event, path);

        var parts = path.split(path_delimiter);

        for (var i = parts.length; i >= 0; i--) {
            var res = parts.slice(0, i);
            res.push("*");

            EventEmitter.prototype.emit.call(this, event + delimiter + res.join(path_delimiter), data, event, path);
            EventEmitter.prototype.emit.call(this, "*" + delimiter + res.join(path_delimiter), data, event, path);
        }
    }
    return this;
};

/**
 * Invoke method
 * @param {String} method Method name
 * @param {Object} [params] Object or Array
 * @param {Function} [callback] Callback function for async methods
 * @return {Amoeba} self for chain execution
 */
Amoeba.prototype.invoke = function() {
    if (this._parent) {
        Array.prototype.unshift.call(arguments, this._path);
        return this.invoke.apply(this.root(), arguments);
    } else {
        var self = this;
        var context = {
            request: {
                path: arguments[0],
                method: arguments[1]
            }
        };
        //Delete method and path from arguments
        Array.prototype.splice.call(arguments, 0, 2);

        var callback = (typeof(arguments[arguments.length - 1]) == "function") ? Array.prototype.splice.call(arguments, arguments.length - 1, 1)[0] : null;

        context.request.arguments = Array.prototype.slice.call(arguments);
        
        //Init response if callback exists
        if (callback !== null) {
            context.response = {
                error: null,
                result: null
            };
        }

        //Chain of responsibility
        var chain = new Chain(self.handlers("before_invoke", context.request.path));
        chain.add(function(context, next) {
            var router = self.route(context.request.path);
            if (router) {
                if (router.invoke) {
                    router.invoke(context, next);
                } else {
                    throw new Error("Path \"" + context.request.path + "\" has no method invoke");
                }
            } else {
                throw new Error("Path \"" + context.request.path + "\" not found");
            }
        });
        chain.add(self.handlers("after_invoke", context.request.path));

        chain.start(context, function(context) {
            //success
            if (callback !== null) {
                callback(context.response.error, context.response.result);
            }
        }, function(context) {
            //interrupted
            if (callback !== null) {
                callback(context.response.error, context.response.result);
            }
        });
        return this;
    }
};
/**
 * Add middleware logic
 * @param {String} type Type of handler. before_invoke | after_invoke | before_event
 * @param {Function} handler Handler
 * @return {Amoeba} self for chain execution
 */
Amoeba.prototype.use = function() {
    if (this._parent) {
        this.root().use(this._path, arguments[0], arguments[1]);
    } else {
        if (!this._handlers[arguments[1]]) {
            this._handlers[arguments[1]] = [];
        }
        this._handlers[arguments[1]].push({
            path: arguments[0],
            handler: arguments[2]
        });
    }
    return this;
};

Amoeba.prototype.handle = Amoeba.prototype.use;

/**
 * Find handlers by type or by type and path
 *
 * @param  {String} type Type of handle
 * @param  {String} path Used path
 * @return {Array}      All founded handlers
 */
Amoeba.prototype.handlers = function(type, path) {
    if (this._parent) {
        return this.root().handlers(type, this._path);
    } else {
        var res = [];
        var all = this._handlers[type] || [];
        for (var i = 0; i < all.length; i++) {
            if (typeof(path) == "undefined" || this.compare(all[i].path, path)) {
                res.push(all[i].handler);
            }
        }
        return res;
    }
};

Amoeba.prototype.compare = function(mask, path) {
    if (path == mask) {
        return true;
    }
    var parts = path.split('.');
    for (var i = parts.length; i >= 0; i--) {
        var res = parts.slice(0, i);
        res.push("*");
        if (res.join(".") == mask) {
            return true;
        }
    }
    return false;
};

Amoeba.prototype.route = function(path) {
    if (this._parent) {
        return this.root().route(path);
    } else {
        if (this._vacuols[path]) {
            return this._vacuols[path];
        }

        var parts = path.split('.');
        for (var i = parts.length; i >= 0; i--) {
            var res = parts.slice(0, i);
            res.push("*");
            if (this._vacuols[res.join(".")]) {
                return this._vacuols[res.join(".")];
            }
        }
        return null;
    }
};

module.exports = exports = Amoeba;