(function(){
angular.module('angoose.ui.directives').directive("deformListing", angList ).directive("angList", angList);

function angList( $templateCache, $routeParams, $compile, $location, $injector, MessageBox, $log ,$route, $ui, $controller, angoose){
    // this is the main controller for the sort-paging-filtering list
    var directive = {
        restrict:'AE'
    };
    directive.controller = function($scope){
        var query = $scope.defineQuery();
        this.refresh = function(){
            query.update(null, true);
        }
    };

    directive.compile = function(element, attrs){
        angoose.logger.trace("In ang-list compile");
        var preLink = function($scope, $element, $attrs){
            /** we do this in prelink because child directives needs the dmeta setup below */
            enterscope($scope, "ang-list  prelink",  $route.current);
            /**@todo: calling custom controller here is kinda not the right angular way, but we augment the $scope */
            // get the configured defaults
            // var ctrl = $ui.getConfig($location.path());
            // if(ctrl){
                // console.log("Calling user defined controller", ctrl)
                // $controller ( ctrl, {$scope: $scope, $element:$element, $attrs: $attrs});
            // }
            // if($routeParams.customController) {
                 // $controller ( ($routeParams.modelName+"-list-"+ $routeParams.customController).toLowerCase(), {$scope: $scope});
            // }
            var dmeta = $scope.defineQuery();
            // $scope.dmeta = $scope.dmeta || {};
            // var dmeta = $scope.dmeta;
            /** order or presedence: custom controller -> directive -> route params  */
            dmeta.modelName =   dmeta.modelName || $attrs.modelName || $routeParams.modelName ;

            try{
                dmeta.modelClass = $injector.get($ui.camelcase( dmeta.modelName  ));
            }
            catch(err){
                console.error("Error in deformListing controller", err)
                MessageBox.error("Model "+ dmeta.modelName+" is not defined")
                return;
            }
            // publish it to the parent scope
            var listName  =$attrs.angList   || $ui.apiCamelcase( dmeta.modelName  ) + "List";
            if($scope.$parent)
                $scope.$parent[listName] = dmeta;
            else
                $scope[listName] = dmeta;

            dmeta.pageTitle = dmeta.pageTitle || $ui.camelcase(dmeta.modelName) + " List"
            dmeta.actionColumn = dmeta.actionColumn === undefined ? true: dmeta.actionColumn;
            if(Array.isArray(dmeta.columns) && dmeta.columns.length > 0){
                var cols = [];
                for(var i=0;i< dmeta.columns.length;i++){
                    var item = dmeta.columns[i];
                    var pathname = typeof(item) == 'string'? item: item.path;
                    var original = $ui.getPathSchema(dmeta.modelClass, pathname);
                    if(typeof(item) == 'object')
                        item = angular.extend( angular.extend({}, original), item)
                    else
                        item = original;
                    if(!item)  item = {path: pathname, options: {sortable:false } }
                    cols.push( item );
                }
                dmeta.columns = cols;
            }
            else
                dmeta.columns =  findTagged(dmeta.modelClass, 'default-list');
            //dmeta.spec = dmeta.spec || { filter:{} }
            var spec = dmeta.spec;
            // controller -> directive
            if(!spec.preset && $attrs.defaultFilter)
                spec.preset =  $scope.$eval($attrs.defaultFilter);

            var defaultSortField = getDefaultSortField(dmeta.modelClass) ;
            //controller -> directive -> default
            spec.sortBy = spec.sortBy || ($attrs.defaultSort? $attrs.defaultSort : '');
            spec.sortBy = spec.sortBy || (  defaultSortField? defaultSortField.path: '' );

            spec.sortDir = spec.sortDir || ($attrs.sortDir?$attrs.sortDir:'');
            spec.sortDir = spec.sortDir || (defaultSortField? defaultSortField.options.defsort: 'asc');

            //console.log($scope.dmeta.columns, " ### COLOUMNS" )
            $scope.remove = function(index){
                //MessageBox.warn("This will delete "+  $scope.instances[index].getDisplayName() +", please confirm");

                MessageBox.confirm("This will delete selected item, continue?", function(){
                    $scope.instances[index].remove( function(err,res){
                        if(!err){
                            MessageBox.success("Operation Successful");
                            $scope.instances.splice(index, 1);
                        }
                    } );
                }, function(){

                })
            }
            $ui.resolveAndCompile($scope, $element, $attrs, dmeta, 'deform.list.tpl');
//
            // $ui.resolveTemplate(element, $attrs, dmeta, 'deform.list.tpl').then(function(em){
                // console.log("Resolved template, compiling(default: list.tpl')", em)
                // if(em){
                    // $element.html("<!-- CLEARED -->"); // first clear the inline template
                    // $element.append(em);
                    // $compile(em)($scope);
                // }
            // }, function(err){
                // console.error("Error resolving template(list.tpl)", err);
            // });

         }
         var postLink = function($scope, $element, $attrs){
            enterscope($scope, "postlink ang listing");
            var spec =   $scope.dmeta.spec;

            $scope.$watch("dmeta.spec", function(newVal, oldVal){
                angoose.logger.trace("Filter changed", newVal, oldVal);
                // handles the search fields, we need to make a copy of the search filters so that it won't trigger the $watch
                var mQuery = angular.extend({}, spec.filter);
                if(spec.preset)
                    mQuery = angular.extend(mQuery, spec.preset); // preset filter takes presedence
                angular.forEach(mQuery, function(val, key){
                   if(val === '$CLEAR$')
                        delete mQuery[key];
                });

                var mSelection = null; //@TBD
                var mOptions = {};  // limit, skip, sort
                if(spec.sortBy){
                    mOptions.sort = (spec.sortDir && spec.sortDir.toLowerCase() == 'desc')? "-":"";
                    mOptions.sort += spec.sortBy;
                }
                if(spec.pageSize) {
                    mOptions.limit = spec.pageSize;
                    if(spec.page > 1)
                        mOptions.skip = (spec.page-1) * spec.pageSize
                }

                angoose.logger.trace("Updating search: ", mQuery ,mOptions, $scope.dmeta.modelName);
                $scope.instances = $scope.dmeta.modelClass.$query(mQuery, mSelection, mOptions )
                $scope.dmeta.modelClass.count(mQuery).done(function(total){
                    //if(err) $log.error("Error getting total. Query: ", mQuery, " Error:", err);
                    spec.$total = total || 0;
                });

            }, true);

            $scope.cells = [];

            // var templateUrl = $attrs.templateUrl;
            // if(!templateUrl && !element.html()){
                // templateUrl = "deform.list.tpl"; // default
            // }
            // if(templateUrl){
                // element.html("<!-- to be replaced by contents from "+  templateUrl+" -->");
                // console.log("Loading templateUrl",  templateUrl);
                // $ui.loadTemplate(templateUrl).then(function(em){
                    // console.log("Compiling template  ", em);
                    // element.append(em);
                    // $compile(em)($scope);
                // }, function(er){
                    // console.log("Failed to load template",  templateUrl)
                // })
            // }

            // $compile template if provided
            // var template = getTemplate(scope.data);
            // element.html(template);
            //

        } // end link
        return {pre:preLink, post:postLink }

    }; // end compile

    return directive;
};  // end directiveFunc

// get a list of fields with a specific tag
function findTagged(modelClass,tag){
    if(!modelClass || !modelClass.schema) return [];
    var cols = [];
    Object.keys(modelClass.schema.paths).forEach(function(path){
        var data = modelClass.schema.paths[path];
        if(data.options.tags && data.options.tags.indexOf(tag)>=0)
            cols.push(data);
    });
    return cols;
}

function getDefaultSortField(modelClass){
    if(!modelClass || !modelClass.schema) return [];
    var ret = null;
    Object.keys(modelClass.schema.paths).forEach(function(path){
        var data = modelClass.schema.paths[path];
        if(data.options.defsort && typeof(data.options.defsort) == 'string')
             ret = data;
    });
    return ret;
}
})(); // scope wrapper

function enterscope(scope, name, arg1) {
	angoose.logger.trace("Entering scope ", name, scope.$id, arg1)
	window['scope' + scope.$id] = scope;
}


var angoose = require("../lib/angoose");
var toolbox = require("../lib/util/toolbox");
var EXTENSION = 'angoose-mongoose';
var path= require("path"),traverse = require("traverse"),hooks= require("hooks"), Q = require("q");
var fs = require("fs"),  _ =require("underscore");

toolbox.patchMongoCallback();

var plugin = {
    postResolveTarget: postResolveTarget,
    postInvoke: postInvoke,
    postPack: postPack,
    postExportModule: decorateMongooseSchema,
    afterFormatError: formatError
};
module.exports = angoose.extension('MongooseExtension', plugin);

function formatError(next, invocation){
    var ex = invocation.packed.exception;
    console.log("Intercepting error", ex);
    // check if it's mongoose's model error
    var errors = toolbox.getter(ex , 'cause.errors');
    if( errors){
        // format model error
        var msg = '';
        Object.keys(errors).forEach(function(key){
            var errobj = errors[key];
            var m = errobj.message+"";
            msg = msg+m+" "
        });
        ex.message = msg;
    }
    next();
};

function decorateMongooseSchema(  client, moduleName){
    var model = angoose.module(moduleName);
    if(!model.schema || !model.schema.paths) return;
    logger().trace("Decorating mongoose model", moduleName);

    var schema = client.schemas[moduleName];
    // automatically publish these instance methods
    var instanceMethods = "save,remove,populate";
    instanceMethods.split(",").forEach(function(m){
        m = m.replace(/\s+/g, '');
        if(!schema.methods[m])
            schema.methods[m]= function remote(){};
    });

    // automatically publish these static methods
    var staticMethods = "populate,find,findOne,findById,findByIdAndRemove,findByIdAndUpdate,findOneAndRemove,findOneAndUpdate,update,remove,count,geoNear,geoSearch,create";
    staticMethods.split(",").forEach(function(m){
        m = m.replace(/\s+/g, '');
        if(!schema.statics[m])
            schema.statics[m]= function remote(){};
    });

    schema.paths = traverse(model.schema.paths).map(function(item){
        if(!item) return;
        if(item.options && typeof(item.options.type) === 'function' ){
            var fn = item.options.type;
            item.options.type = fn.name || fn.toString();
        }
        else if(typeof(item) == 'function')
            this.update( 'not-supported' );
        if(item.requiredValidator) delete item.requiredValidator;
        if(item.enumValidator) delete item.enumValidator;
        if(item.validators) delete item.validators; /**@todo validators are not supported yet*/
    });
    schema.options = traverse( model.schema.options).clone();
//    filtPaths(schema);
}
function filtPaths(schema){
    traverse(schema).forEach(function(item){
        if((this.parent&&this.parent.parent&&this.parent.parent.key=='paths')
                ||(this.parent&& (this.parent.isRoot || this.parent.key == 'schema')))
        {
            if(_.indexOf(['moduleName','methods','statics','paths','enumValues','isRequired','schema','caster','path','options','instance'],this.key)<0){
                this.remove();
            }
        }
    })
}

function logger(){
    var logger = angoose.getLogger('angoose'); return logger;
}

function postResolveTarget(next, invocation){
    if(!invocation.target || invocation.static || ! invocation.instance || !invocation.instance._id ) return next();
    logger().debug("postResolveTarget for mongoose models");

    var _id = invocation.instance._id;
    logger().trace("Loading pristine instance as base",invocation.clazz, _id);
    var modelClass = angoose.module(invocation.clazz);
    modelClass.findById(_id,   function mongoCallback(err, pristineInstance){
       if(err || !pristineInstance){
           logger().error("Failed to load model by id", _id);
           return next(err);
       }
       var pristineObject = pristineInstance.toObject();
       var schema = modelClass.schema;
       Object.keys(schema.paths).forEach(function(path){
           if (/^_/.test(path)) return;
           //var pathSchema = schema.paths[path];
           //var ref = schemaUtil.getReference(pathSchema);
           if (/^_/.test(path)) return;
           var newVal = toolbox.getter(invocation.instance, path);
           var oldVal  =  toolbox.getter(pristineObject, path);
           if(!_.isEqual(oldVal, newVal) &&  newVal !==  undefined ){ /**@todo: empty value from client side doesn't mean set to empty, need to implement the $dirty, modified' */
                if(newVal == '$CLEAR$') newVal = null;
                pristineInstance.set(path,newVal);
                logger().trace("seting", path, ": ",oldVal, " --> ", newVal, " NOW dirty? ",  pristineInstance.isModified(path));
           }
       });
       invocation.target = pristineInstance;
       next();
    });
}

function postInvoke (next, invocation){
    var result = invocation.result;

    if(result && result.exec && (result instanceof angoose.getMongoose().Query) ){ // mongoose promise
        logger().debug("Return is mongoose query, call exec(). Conditions: ", result._conditions );
        result.exec( function(err, ret){
            if(err) return next(err);
            invocation.result = ret;
            next();
        });
        return;
    }

    function checkForModelError(result ){
        var target = invocation.static ? null : invocation.target;
        var err = ( target && target.errors) || (result && result.errors);
        if(err)
        {
            // temp error handling
            var msg = '';
            Object.keys(err).forEach(function (errItem) {
                msg += ex.message?"; ":"";
                msg += err[errItem].message;
            })
            msg = msg || err.toString();
            return msg;
        }
    }

    process.nextTick(function waitForModelError(){
        /** the model error is emitted in next tick */
        var ex = checkForModelError(result);
        if(ex){
            logger().debug("Detected model error", ex);
            return next(ex);
        }
        next();
    });

}; /**  end invoke */

function postPack(next, invocation){
    //console.log("In mongoose post pack", invocation)
    if(!invocation.redacted) return next();
    var type = getValueType(invocation.redacted);
    if(type) invocation.packed.datatype = type;
    next();
}

function getValueType(obj){
    if( obj.schema && obj.schema.paths && obj.schema.methods ) return 'model';
    if(Array.isArray( obj )){
        if(obj.length && getValueType(obj[0]) == 'model') return "models";
    }
    return null;
}


/*

A simple argv parser

© 2017 - Guillaume Gonnet
License GPLv2

Sources at https://github.com/ParksProjets/C-Preprocessor

*/

// List of short/long name commands
var short = {};
var long = {};

// Remaning parameters
exports.argv = [];

// On error callback
exports.error = null;

// Emit an error
function error(msg) {
	if (exports.error)
		exports.error(msg);
	else
		console.log('Error: ' + msg);
}

// Current length & position
var count = 0;
var n = 0;

// Get the next argument
function next() {
	return process.argv[n++];
}

// Add a new commands
exports.add = function add(name, longName, length, callback) {

	var obj = {
		length: length,
		callback: callback
	};

	short[name] = obj;
	long[longName] = obj;
};

// Call a command
function call(name, isLong) {

	var obj = isLong ? long[name] : short[name];
	if (!obj)
		return error(`Unknown argument '${name}'`);

	if (n + obj.length > count)
		return error(`Too few arguments after '${name}'`);

	var arr = process.argv.slice(n, n + obj.length);
	n += obj.length;

	obj.callback(arr);
};

// Start parsing argv
exports.parse = function() {

	count = process.argv.length;
	n = 2;

	var s;
	while ((s = next()) != undefined) {

		if (s.startsWith('--'))
			call(s.substr(2), true);

		else if (s.startsWith('-'))
			call(s.substr(1), false);

		else
			exports.argv.push(s);
	}
};


// This is the base class for Angoose model classes and service classes.
// This is used internally and it's here only for listing the APIs common
// to all Model and Service classes.

var logger = require("log4js").getLogger("angoose");
var _ = require("underscore");
var Class = require("./util/classy");

var toolbox = require("./util/toolbox");

// ### API Rereferences
// **getContext()**
//
// This method allows you obtain the execution context.
// See [Context](Context.html) for more.
//
// Note this method is also available on the class(function) as well. i.e.,
//
//      /** MyModel is subclass of Remotable */
//      var instance = new MyModel();
//      /** Following yields true */
//      MyModel.getContext() === instance.getContext();
//
var getContext = function(){
    return require("./angoose").getContext()
}

var  Remotable = Class.$extend({});
Remotable.prototype = {
    getContext: getContext
}

Remotable._angoosemeta ={ baseClass: 'Remotable' }
Remotable.extend = extend;
Remotable.getContext = getContext;
// ** extend **
//
// This static method is used to create Angoose model or service classes.
// All classes created using this method is a subclass of `Remotable`
function extend(target, options){
    options = options||{};
    var parentClass = this;
    logger.trace("Extending from ",   parentClass._angoosemeta.name, options);
    var rv = null;
    if(typeof (target) == 'function'){
        rv = target;
        mixinInstance(parentClass, rv, options);

        /**@todo: temp hack */
        bindMongooseMethods(rv);
    }
    else{
        /** schema object */
        rv = parentClass.$extend( target );
    }

    /** mixin Angoose class level functions */
    rv = mixinStatic(parentClass, rv, options);
    if(rv._angoosemeta.name){
        /**  register it with Angoose */
       //require("./angoose").registerClass(rv._angoosemeta.name, rv);
    }
    return rv;
}
function mixinInstance(parentClass, subClass, options){
    subClass.prototype.getContext = getContext;

    /** look for static classes*/

}
function mixinStatic(parentClass, subClass, options){
    /** mixin Angoose functions to the new class */
   /** if(subClass.extend || subClass.getContext ) throw "Class already has 'extend' or 'getContext' property/method" */

    /** mixing static properties from parentClass to subClass */
    for(var prop in parentClass){
        var val = parentClass[prop];
        if(!parentClass.hasOwnProperty(prop) || typeof(val) != 'function') continue;
        if(isRemotableReserved(prop)) continue;
        if(subClass[prop]) {
            logger.warn("Cannot mixin subclass: property '"+ prop+"' already exists on subclass");
            throw "Subclass already has property '"+ prop+"' defined.";
        }
        //logger.trace("Mixing: ", prop)
        subClass[prop] = val;
    }
    // subClass.extend = extend;
    // subClass.getContext = getContext;
    subClass._angoosemeta = _.extend({}, parentClass._angoosemeta);
    subClass._angoosemeta = _.extend(subClass._angoosemeta, options)

    return subClass;
}

function isRemotableReserved(methodName){
    /** check if method is reserved by the Remotable interface */
  return methodName.indexOf("_") == 0 || methodName.indexOf("$") == 0 || ['constructor',   'isRemotableReserved'].indexOf(methodName)>=0;
}
/** a$ is Angoose's internal property*/
//Remotable.prototype._angoosemeta = {};

Remotable.getContext = getContext;
Remotable.isRemotableReserved=isRemotableReserved;

module.exports = Remotable

Remotable.mixin = function(opts, target){
    opts = opts || {};
    target.getSchema = getSchema;
    target._angoosemeta  = toolbox.merge(target._angoosemeta , opts);
    target._angoosemeta.baseClass = target._angoosemeta.baseClass || 'Service';
    target.getContext = getContext; //deprecated
    target.config = config;
    if(target._angoosemeta.baseClass == 'Model')
        bindMongooseMethods(target);
}

function config(path, opts){
    var conf = this._angoosemeta;
    switch(arguments.length){
        case 0: return conf;
        case 1: return toolbox.getter(conf, path);
        case 2: toolbox.setter(conf, path, opts);
    }
    return this;
}
function getSchema(){
    var thisClass = this;
    var schema = {statics: {}, methods:{} };
    for(var name in thisClass){
        var val = thisClass[name];
        if( filter(name) ) continue;
        if(typeof (val) != 'function') continue;
        schema.statics[name] = val;
    }
    if(typeof(thisClass) == 'function'){
        schema.methods = _.extend({}, thisClass.prototype)
    }
    return schema;
}

function filter(methodName){
     if(Remotable.isRemotableReserved(methodName)) return true;
     if(['extend', 'getContext', 'getSchema','mixin','toJSON' ].indexOf(methodName)>=0) return true;
     return false;
}

function bindMongooseMethods(modelClass){
    var staticMethods = "populate,find,findOne,findById,findByIdAndRemove,findByIdAndUpdate,findOneAndRemove,findOneAndUpdate,update,remove,count,geoNear,geoSearch,aggregate".split(",");
    _.each(staticMethods, function(method){
        var fn = modelClass[method];
        if( typeof(fn) == 'function'){
            modelClass[method] =  toolbox.bindCallback(modelClass, fn )
        }
    });
    var instanceMethods = "save,remove,populate,update".split(",");
    _.each(instanceMethods, function(method){
        var fn = modelClass.prototype[method];
        if( typeof(fn) == 'function'){
            modelClass.prototype[method] =  toolbox.bindCallback(null, fn )
        }
    });
}


var logger = require("log4js").getLogger("angoose"), path=require("path"), fs=require("fs");
var _ =require("underscore");
var toolbox = require("./util/toolbox");
var traverse  = require("traverse");

function Bundle(){}
Bundle.initHooks = initHooks;

var hooks = {};
function initHooks(){
    hooks.pre = {};
    hooks.post = {};
    ['generateClient','configClient','exportModules','exportModule','createBundle'].forEach(function(name){
        hooks.pre[name] = [];
        hooks.post[name] = [];
    })
}

// synchronous hooks

function addHook(loc, method, func){
    var tmp = hooks[loc][method];
    logger.debug("ADdding bundle hook to", loc, method, tmp );
    if(!tmp) return false;
    tmp.push(func);
    return true;
}
Bundle.pre = function(method, func){
    return addHook('pre', method, func)
}
Bundle.post = function(method, func){
    return addHook('post', method, func)
}

function invokeHook(loc, method, arg1, arg2){
    //console.log("***** Bundle.invokeHook ******* ", loc, method)
    var funcs = hooks[loc][method] || []
    funcs.forEach(function(func){
        logger.trace("calling hook", loc, method, func.name);
        func.call(null, arg1, arg2);
    });
}

function angoose(){
    return require("./angoose");
}
//AngooseSchema.prototype =   {
    //stringify: stringify,
    //parseDeclaredArguments:toolbox.parseDeclaredArguments,
    //getReference:getReference,
    //getFunctionName: getFunctionName
//};
var proto = Bundle.prototype;
/**
 * prepare the schema for the module
 *
 * @param {String} moduleName
 * @param {Object} module
 */
proto.exportModule = function(client, moduleName){
    invokeHook('pre', 'exportModule', client, moduleName);

    logger.trace("in exportModule", moduleName);
    if(client.schemas[moduleName]) return; // already handled
    var mod = angoose().module(moduleName);
    var schema = toolbox.exportModuleMethods(moduleName, mod);
    if(!schema) logger.error("Module", moduleName, " has no schema, export failed", mod);
    else client.schemas[moduleName] = schema;

    invokeHook('post', 'exportModule', client, moduleName);
};

proto.exportModules = function(client){
    invokeHook('pre', 'exportModules', client);

    logger.debug("Exporting all modules");
    client.schemas = client.schemas || {};
    var me = this;

    var names = angoose().moduleNames();
    var total = names && names.length;
    if(!total) return;

    names.forEach(function(moduleName){
        var mod = angoose().getClass(moduleName);
        if(mod.config && mod.config("visibility") === 'local') return;

        me.exportModule(client, moduleName);

    }) ;

    invokeHook('post', 'exportModules', client);
};

proto.createBundle = function(client){
    logger.debug("Bundle client files");

    if(client.source) return; // already generated source
    var jsonSchemas = toolbox.stringifySchema( client.schemas );
    var template = path.resolve(__dirname , "client/angoose-client.js");
    var content = fs.readFileSync(template , 'ascii');
    content = content.replace("/**SCHEMA_PLACEHOLDER*/",  jsonSchemas);
    content = content.replace("/**CONFIG_PLACEHOLDER*/", JSON.stringify( client.options ));

    /**  include client specific extensions*/
    var clientModuleFile = path.resolve(__dirname , "client/angoose-angular.js");
    content += fs.readFileSync(clientModuleFile, 'ascii');

    clientModuleFile = path.resolve(__dirname , "client/angoose-jquery.js");
    content += fs.readFileSync(clientModuleFile, 'ascii');

    clientModuleFile = path.resolve(__dirname , "client/angoose-node.js");
    content += fs.readFileSync(clientModuleFile, 'ascii');

    client.source = content;
}

/**
 * This method configures an options object which will be served as Angoose client's initial configurations
 * Must set the `client.options` property after the method finishes
 */
proto.configClient = function(client){
    logger.debug("Setup client options");
    client.options = client.options || {};

    var angooseOpts = angoose().config();
    var clientOptionNames = ['url-prefix', 'logging'];

    _.extend(client.options, angooseOpts.client);
    client.options.urlPrefix = angooseOpts['url-prefix'];

    clientOptionNames.forEach(function(key){
        client.options[key] = angooseOpts[key];
    });
    //client.options.logging = angoose().config()['logging'] || 'INFO';
};
/**
 */
proto.generateClient = function(clientData){
    invokeHook('pre', 'generateClient', clientData);
    // client side configurations

    invokeHook('pre', 'configClient', clientData);
    this.configClient(clientData);
    invokeHook('post', 'configClient', clientData);

    invokeHook('pre', 'exportModules', clientData);
    this.exportModules(clientData);
    invokeHook('post', 'exportModules', clientData);

    invokeHook('pre', 'createBundle', clientData);
    this.createBundle(clientData);
    invokeHook('post', 'createBundle', clientData);

    invokeHook('post', 'generateClient', clientData);

};
//@tood: ,serializeModules,serializeModule,prepareSchema
// Bundle.hookables = 'generateClient,configClient,exportModules,exportModule,createBundle'.split(",");
// toolbox.addHookPoints(Bundle);

Bundle.typeOf = toolbox.typeOf;
module.exports = Bundle;


(function(){
angular.module('angoose.ui.directives').directive('deformField', angField).directive('angField', angField);

function angField($compile, $templateCache, $interpolate, $injector, $controller, $ui, angoose) {

  // Find the "input" element in the template.  It will be one of input, select or textarea.
  // We need to ensure it is wrapped in jqLite\jQuery
  function findInputElement(templateElement) {
    return angular.element(templateElement.find('input')[0] || templateElement.find('select')[0] || templateElement.find('textarea')[0]);
  }

  function findLabelElement(templateElement) {
    return templateElement.find('label');
  }

  // Search through the originalDirective's element for elements that contain information about how to map
  // validation keys to messages
  function getValidationMessageMap(originalElement) {
    // Find all the <validator> child elements and extract their (key, message) info
    var validationMessages = {};
    angular.forEach(originalElement.find('validator'), function(element) {
      // Wrap the element in jqLite/jQuery
      element = angular.element(element);
      // Store the message info to be provided to the scope later
      // The content of the validation element may include interpolation {{}}
      // so we will actually store a function created by the $interpolate service
      // To get the interpolated message we will call this function with the scope. e.g.
      //   var messageString = getMessage(scope);
      validationMessages[element.attr('key')] = $interpolate(element.text());
    });
    return validationMessages;
  }

  // Find the content that will go into the new label
  // Label is provided as a <label> child element of the original element
  function getLabelContent(element) {
    var label = element.find('label');
    return label[0] && label.html();
  }

  function getTemplateContent(element){
      var em = element.find('template');
      if(!em) return null;
      if(!em.attr('name') && ! em.attr("url"))
        return em[0] && em.html();
      return em.attr('name') || em.attr('url');
  }

  return {
    restrict:'E',
    scope:{
      instance:'=',
      path:'=',
      modelSchema:'=',
      fieldSchema:'=',
      readonly:'@',
      itemIndex:'=' // for array index
    },
    //require:'?ngModel',
    priority: 100,        // We need this directive to happen before ng-model
    terminal: true,       // We are going to deal with this element
    compile: function(element, attrs) {
      if ( attrs.ngRepeat || attrs.ngSwitch || attrs.uiIf ) {
        throw new Error('The ng-repeat, ng-switch and ui-if directives are not supported on the same element as the field directive.');
      }
      if ( !attrs.ngModel ) {
        //throw new Error('The ng-model directive must appear on the field element');
      }

      // Extract the label and validation message info from the directive's original element
      var validationMessages = getValidationMessageMap(element);
      var labelContent = getLabelContent(element);

      // Clear the directive's original element now that we have extracted what we need from it
      element.html('');

      return function postLink(scope, element, attrs, ngModelController) {

        window.pscope = scope;
        if(scope.path && scope.path.indexOf("_") == 0) return;

        enterscope(scope, "deform-field "+ scope.path)

        var customTemplate  =  attrs.template;
        var customController = attrs.controller;
        var customDirective = attrs.directive;

        var modelClass = scope.instance && scope.instance.constructor;

        //var schema = getSchema( scope.instance).paths[scope.path] ;
        var modelSchema = scope.modelSchema || (modelClass && modelClass.schema);
        //if(!modelSchema) console.error("Model schema lost in scope ", scope.$id, "parent is ", scope.$parent.$id)

        var schema = scope.fieldSchema || ( modelClass && modelClass.schema.paths[scope.path]);
        if(!schema) {
            return console.error("Missing schema for path", scope.path);
        }
        schema.options = schema.options || {};

        var directive = customDirective || mapDirective(scope.path, schema, modelSchema, scope.itemIndex)

        var template = customTemplate || mapTemplate(scope.path, schema, modelSchema);
        angoose.logger.trace("Field ", scope.path,  "template", template );

        var labelContent =  attrs.label == undefined? ( schema.options.label ||scope.path) : attrs.label;

        var childScope = scope.$new();
        // Generate an id for the field from the ng-model expression and the current scope
        // We replace dots with underscores to work with browsers and ngModel lookup on the FormController
        // We couldn't do this in the compile function as we need to be able to calculate the unique id from the scope
        childScope.$fieldId = (scope.path+ (scope.itemIndex || "") ).replace('.', '_').toLowerCase() + '_' + childScope.$id;

        function handleDirective(directive, childScope){
            var html = '<directive-name path="path" model-schema="model-schema" field-schema="fieldSchema" instance="instance"></directive-name>';
            html = html.replace('directive-name', directive);
            var directiveElement = angular.element(html);
            var labelElement = directiveElement.find('label');
            labelElement.attr('for', childScope.$fieldId);
            // Update the label's contents
            labelElement.html(labelContent  );
            element.append(directiveElement);
            $compile(directiveElement)(childScope);
        }

        angoose.logger.trace("Created child scope for deform-field ", scope.path,  childScope.$id, "using directive: ",directive)
        if(directive){
            handleDirective( directive, childScope);
        }
        else
        {

            // Load up the template for this kind of field, default to the simple input if none given
            $ui.loadFieldTemplate(template ).then(function(templateElement) {
              // Set up the scope - the template will have its own scope, which is a child of the directive's scope

              // Attach a copy of the message map to the scope
              childScope.$validationMessages = angular.copy(validationMessages);

              childScope.$fieldLabel = labelContent;

              // Update the $fieldErrors array when the validity of the field changes
              childScope.$watch('$field.$dirty && $field.$error', function(errorList) {
                  //angoose.logger.debug("Got error", errorList);
                childScope.$fieldErrors = [];
                angular.forEach(errorList, function(invalid, key) {
                  if ( invalid ) {
                    childScope.$fieldErrors.push(key);
                  }
                });
              }, true);

              // Copy over all left over attributes to the input element
              // We can't use interpolation in the template for directives such as ng-model
              var inputElement = findInputElement(templateElement);
              angular.forEach(attrs.$attr, function (original, normalized) {
                var value = element.attr(original);
                inputElement.attr(original, value);
              });
              var ngModelVal = "instance."+ scope.path + (scope.itemIndex == undefined? "": "["+ scope.itemIndex+ "]" );
              inputElement.attr("ng-model", ngModelVal);
              //console.trace("NGMODEL #### ", inputElement.attr('ng-model'))

              // Wire up the input (id and name) and its label (for).
              // We need to set the input element's name here before we compile the template.
              // If we leave it to be interpolated at the next $digest the formController doesn't pick it up
              inputElement.attr('name', childScope.$fieldId);
              inputElement.attr('id', childScope.$fieldId);
              if(scope.readonly || schema.options.readonly === true) inputElement.attr('readonly', "true");
              schema.options.required && inputElement.attr('required', 'true');
              var labelElement = templateElement.find('label');
              labelElement.attr('for', childScope.$fieldId);
              // Update the label's contents
              labelElement.html(labelContent);

              // Place our template as a child of the original element.
              // This needs to be done before compilation to ensure that it picks up any containing form.
              element.append(templateElement);

                // filed controller
                //var fieldController = customController || "dfController"+ camelcase(template);
                var fieldController = customController || "dfc-"+template;

                try{
                    $controller(fieldController, {$scope: childScope, $schema: schema, inputElement: inputElement, templateElement: templateElement})
                    angoose.logger.trace("Invoked custom controller", fieldController);
                }
                catch(err){
                    if((err+"").indexOf("is not a function")<0)
                        angoose.logger.error("fieldControoler error", err)
                }
                // We now compile and link our template here in the postLink function
                // This allows the ng-model directive on our template's <input> element to access the ngFormController
                $compile(templateElement)(childScope);

                // Now that our template has been compiled and linked we can access the <input> element's ngModelController
                childScope.$field = inputElement.controller('ngModel');
            });
          } // end loadTemplate
      }; // end postLink
    }
  };
}
// ** mapDirective **
//
// Default mapping based on schema definition:

// - list of sub schemas: deform-sublist
// - single subschema object: deform-subschema

function mapDirective(path, pathSchema, modelSchema, itemIndex){
    if( itemIndex !== undefined) return null; // we're in a array
    if(pathSchema && pathSchema.options && Array.isArray(pathSchema.options.type)){
        if( pathSchema.schema)
            return "deform-sublist"

        if(pathSchema.caster && (! pathSchema.caster.options || !pathSchema.caster.options.ref)){ // !pathSchema.options.ref filters out CustomRef, ugly!
            /** array of simple types */
           //console.log("Simple array type!!!",pathSchema, pathSchema.options.ref, pathSchema.options.type);
           return "ang-array"
        }

    }
    else  if(  pathSchema.schema)
        return "deform-subschema"
    return null;
}

// ** mapTemplate **
//
// Default mapping for Mongoose schema type -> form field
//
// - ObjectID: selector
// - Boolean: Checkbox
// - String: input
// - Number: input
// - Date: ?
// - Array of simple types: selector multi
// - Array of ref objects: selector multi
// - String/Number with enum values:  Select
// - Mixed: ??
//
function mapTemplate(path, pathSchema, modelSchema){
    if(pathSchema.options.template) return pathSchema.options.template;
    if(pathSchema.options.multiline) return "textarea"
    var template = 'input';
    var opts = pathSchema.options || {};
    //@todo: refactor them into subclasses

    switch(pathSchema.instance || (pathSchema.options && pathSchema.options.type)){
        case 'ObjetcID':
            if(opts.ref)    template = 'selector';
            break;
        case 'Boolean':
            template = 'checkbox';
            break;
        case 'Date':
        case 'String':
        case 'Number':
        default:
            break;
    }
    if(getRef(pathSchema))
        template = "selector";
    if(Array.isArray(opts['enum']) && opts['enum'].length>0){
        template = "select";
    }
    if(Array.isArray(opts.type )){
        if(pathSchema.caster){
            /** array of simple types */
           // console.log("Simple types", pathSchema)
        }
    }

    angoose.logger.trace("Path ", path, " Type ", pathSchema , " template: ", template)
    return template;
}

function getRef(pathSchema){
    var opts = pathSchema.options;

    /** Mongoose, array of ObjectIDs */
    if( Array.isArray(opts.type) && opts.type.length>0  &&  opts.type[0] && opts.type[0].ref )
        return opts.type[0].ref;
    /** Single ObjectID Reference*/
    if(pathSchema.instance == 'ObjectID' && opts.ref) return opts.ref;

    if(pathSchema.options.ref && pathSchema.instance == 'CustomRef'){
        /** deform custom ref*/
       return pathSchema.options.ref;
    }
    /** deform rich reference, array */
    if(Array.isArray(opts.type ) && pathSchema.caster &&
            pathSchema.caster.instance == 'CustomRef' && pathSchema.caster.options.ref ){
        return pathSchema.caster.options.ref;
    }

    return null;
}
function camelcase(str){
    return str? str.substring(0,1).toUpperCase() + str.substring(1): str;
}
})();  // end enclosure


// Angoose main module
//
//      var angoose = require("angoose");
//      angoose.init(expressApp, { })
//
var ROOT = process.cwd();
var path= require("path"),traverse = require("traverse"),hooks= require("hooks"), Q = require("q");
var fs = require("fs"),logging = require("log4js"), _ =require("underscore");

var Pipeline = require('./Pipeline');
var Bundle = require("./Bundle");

var pjson = require('../package.json'), toolbox = require("./util/toolbox");
var async = toolbox.async, domain=require("domain");
var logger = logging.getLogger('angoose');
logger.setLevel('INFO');
var domainLogger = logging.getLogger('angoose-domain');
domainLogger.setLevel('INFO');
var DEFAULT_OPTIONS = {
    'core-extensions':['angoose-mongoose'],
    'url-prefix':'/angoose',
    'client-file': ROOT+'/angoose-client-generated.js',
    'module-dirs': './models',
    'mongo-opts':'localhost:27017/test',
    'logging':'INFO'
}
/** Variables */
var options = {};
var beans = {}; /** this holds all Angoose classes*/

// ### API References

// ** init(app, options) **
//
// Initialize Angoose. This function should be called in the express app
//
// * @app: Express app, for now
// * @options: Angoose settings:
//     - module-dirs, optional, default to ./models
//     - url-prefix, optional, default to /angoose
//     - mongo-opts, optional. Provide this if you want Angoose to make the connection
//
function init(app, conf, force) {
    if(this.initialized && !force) return;

    //beans = {};
    /** configurations*/
    initialConfig(conf);

    logger.info("Angoose Initialization Start");
    logger.trace("Init options:", conf);

    /** connect to Mongo if necessary */
    connectMongo(options);

    /** register initial hooks */
    //registerHooks();

    /** pre-load models/services from directories */
    harvestModules(options);

    /** plugin extensions */
    hookupExtensions();

    /** build client side schemas */
    generateClient();

    /** configure the routes for handling RMI and client loading*/
    /**@todo: middleware*/
    configureRoutes(app, options);

    logger.info("Angoose Initialization Complete");
}

// ** module(name, function_or_object) **
//
// Retrieve an Angoose module or register one
//
// If only one argument, name is provided, returns the registered module with that name. This form is same as `angoose.getClass()`
//
// If two arguments are provided, register the function/object as Angular module under that name.
//

function lookupOrRegister(name, target){
    if(arguments.length == 0) return null;
    if(arguments.length == 1) return getClass(name);
    if(arguments.length == 2) return registerClass(name, target);
}

function registerExtension(name, module){
     if( typeof(name) =='object' )
        name.isExtension = true;
     else
        name = {name: name, isExtension:true}
     return registerClass(name,module);
}

function hasModule(name){
    return beans[name] ? true : false;
}

function getClass(name){
    name = toolbox.camelcase(name);
    if(!beans[name]) throw "Class '"+ name+"' is not found. Check log to see if class is loaded successfully "
    return beans[name];
}

// register module with angoose so it knows to publish it
function registerClass(nameOrOpts, claz){
    var opts = typeof(nameOrOpts) == 'object'?nameOrOpts: {name: nameOrOpts};
    var className = opts.name;
    if(!className) throw "Missing module name: "+ className
    if(beans[className])
        logger.warn("Overriding existing bean: ", className);
    if(claz._angoosemeta && (claz._angoosemeta.baseClass == 'Service' || claz._angoosemeta.baseClass == 'Model') ){
        // already mixed
    }
    else{
        if(typeof(claz) === 'function' && claz.schema  && claz.modelName )
            opts.baseClass = 'Model';
        else if(claz instanceof getMongoose().Schema){
            opts.baseClass = 'Model';
            claz = getMongoose().model(className, claz);
        }
        else
            opts.baseClass = 'Service';
        angoose.Remotable.mixin(opts, claz);
    }
    _.extend(claz._angoosemeta, nameOrOpts);

    beans[className] = claz;
    //if(!nameOrOpts.isExtension)
        logger.debug("Registered module", claz._angoosemeta.baseClass, className);
    return claz;
    // if(claz._angoosemeta && (claz._angoosemeta.baseClass == 'Service' || claz._angoosemeta.baseClass == 'Model') ){
//
    // }
    // else{
        // throw "Invalid class: must be a Model or Service class: " + claz;
    // }
}

// ** getContext() **
//
// Returns the current execution context.
//
// This methods returns a Context object which allows you to get a reference to the current request object
// and/or login user's Principal object. If the callee isn't inside Angoose execution context, an error will
// be thrown. See [Context](Context.html) for more.
function getContext(){

    if(!domain.active || !domain.active.context){
        if(this.mockContext) return this.mockContext

        logger.error("getContext called but no active domain", domain.active);
        logger.error("Caller is ",   arguments.callee && arguments.callee.caller && arguments.callee.caller.name, arguments.callee && arguments.callee.caller );
        throw "Context not available. This may happen if the code was not originated by Angoose";
    }

    return domain.active.context;
}

function setContext(ctx){
    this.mockContext = ctx;
}
function testContext(name){

    if(!domain.active){
        domainLogger.debug("-------------------------------------- TEST-CONTEXT ", name, "NO ACTIVE DOMAIN");
        return;
    }
    if(!domain.active.context){
        domainLogger.debug("---------------------------------------TEST-CONTEXT ", name, "NO CONTEXT in active domain");
        return;
    }
    domainLogger.debug("-------------------------------------- TEST-CONTEXT ", name, "  OK ");
}

// ** defer() **
//
// Convenience wrapper for `Q.defer()`, returns a deferred object
//
function defer(){
    return Q.defer();
}

function configureRoutes(app, options){
    if(!app){
        logger.warn("app not provided, RMI functionality is disabled");
        return;
    }
    /** this is the main RMI endpoint */
    app.post(options['url-prefix']+"/rmi/:model/:method", Pipeline.accept);
    /** @todo: use static serving to enable cache //app.get("/angoose/AngooseClient.js", express.static(options.clientFile)); */
    app.get(options['url-prefix']+"/angoose-client.js", function(req, res){
        logger.debug("Handling AngooseClinet.js load request");
        var filename = options['client-file'] ;
        var content = fs.readFileSync(filename , 'ascii');
        res.set('Content-Type', 'application/javascript');
        res.send(200,   content );
    });
    app.get(options['url-prefix']+"/angoose-client.min.js", function(req, res){
        logger.debug("Handling AngooseClinet.js.min.js load request");
        var filename = options['client-file']+".min.js" ;
        var content = fs.readFileSync(filename , 'ascii');
        res.set('Content-Type', 'application/javascript');
        res.send(200,   content );
    });
    app.get(options['url-prefix']+"/angoose-client.js.map", function(req, res){
        logger.debug("Handling AngooseClinet.js.min.map load request");
        var filename = options['client-file']+".min.map" ;
        var content = fs.readFileSync(filename , 'ascii');
        res.set('Content-Type', 'application/octet-stream');
        res.send(200,   content );
    });
}

//** bind(func) **
//
// Bind the async callback function with the active domain so we dont' lose the context.
//
// Necessary for Mongoose (and maybe other async modules) callbacks.
//
function inContext(fn){
    /** there is a known issue with CLS that it does not work with MongoDB,
     * needs to bind the callback with the CLS context
     * https://github.com/othiym23/node-continuation-local-storage/issues/6
     * */
    if(domain.active) return domain.active.bind(fn);
    return fn;
}

function scanModelFiles(dirs){
    var dirs = _.isArray(dirs)? dirs: [dirs];
    var files = [];
    files.push(path.resolve( __dirname , '../models/SampleUser.js')); // sample model

    function scanDir(dirname){
        if(!dirname ||   dirname.indexOf("node_modules")>=0) return;
        logger.debug("Scanning directory for modules: ", dirname);
        if(fs.existsSync(path.resolve(dirname, 'index.js'))){
            files.push(path.resolve(dirname, 'index.js') );
            return;
        }
        fs.readdirSync( dirname  ).forEach(function(file) {
            var fullpath = path.resolve(dirname,file);
            if(! fs.statSync(fullpath).isFile()) scanDir( fullpath );
            else if (file.match(/.+\.js/g) !== null) {
                files.push(fullpath);
            }
        });
    }
    dirs.forEach(function(dirname){
        scanDir(dirname);
    });
    return files;
}

function harvestModules(options){
    logger.debug("Harvesting modules");

    logger.debug("Loading all extensions");
    var extensions = options['core-extensions'].concat(options.extensions || []);
    var extensionFiles = _.map(extensions, function(ext){
        var coreExtension = path.resolve(__dirname,  "../extensions/"+ ext);
        if(typeof(ext) == "string" &&
                ( fs.existsSync( coreExtension +".js") ||  fs.existsSync(coreExtension)   ) ){
            logger.trace("Load core extension", ext);
            return coreExtension
        }
        return ext;
    });

    logger.debug("Loading all modules");

    var files = extensionFiles.concat(  scanModelFiles( options.modelDir || options['module-dirs'] ) );
    files.forEach(function(file) {
            try{
                if(typeof(file === 'string')){
                    logger.trace("Loading module file", file);
                    require(file);
                }
            }
            catch(ex){
                 logger.error("Skipping file ", file, " due to error: ", ex);
            }
    });
    logger.debug("Adding all mongoose models");
    _.each(getMongoose().modelNames(), function(modelName){
        if(!hasModule(modelName)){
            registerClass( modelName,  getMongoose().model(modelName));
        }
    });
};
// ** geneateClient() **
//
// Generates the client file to be served as the contents of resource `/angoose/angoose-client.js`
function generateClient(){
    logger.debug("Generating angoose client file: ");
    var bundle = new Bundle();
    var client = {};
    bundle.generateClient(client);
    var filename = angoose.config()['client-file'];
    writeToFile(filename, client.source)
    //compressFile(filename, client.source);
    logger.info("Generated  angoose client file: ", filename);
    return client.source;
}

function compressFile(filename,source){
    var UglifyJS = require("uglify-js");
    var mapFile=filename+".map";
    var minFile=filename+".min.js";
    var result = UglifyJS.minify(source, {fromString:true,outSourceMap:mapFile});
    var map=JSON.parse(result.map);
    map.sources=["angoose-client.js"]
    var code = '//# sourceMappingURL=angoose-client.js.map\n'+result.code;
    writeToFile(minFile,code);
    writeToFile(mapFile,JSON.stringify(map));
}
function writeToFile(filename, content){

    if(filename.indexOf("/")>=0){
        // make sure directory exists
        var outputDir = filename.replace(/^(.*)\/[^\/]+$/, "$1");
        if(outputDir && !fs.existsSync){
            logger.debug("Creating dir", outputDir)
            fs.mkdirSync(outputDir);
        }
    }
    fs.writeFileSync(filename, content);
    logger.debug("Generated the client file:", filename);
}
/**
 * This is mostly used by tests
 */
function getClient(forceGenerate){
    if(!angoose.initialized ||  forceGenerate)
        generateClient();
    return require(options['client-file']);
}
function connectMongo(options){
    var mongoose = getMongoose();
    if(mongoose.connection && mongoose.connection.readyState > 0 ){
        logger.debug("Found Mongoose connection");
        return;
    }
    try{
        logger.debug("Connecting to mongodb", options.mongo_opts || options['mongo-opts'])
        mongoose.connect( (options.mongo_opts || options['mongo-opts']) , function(err){
            if(err) logger.debug("Error connecting to Mongo:", err)
            else logger.debug("Connected to MongoDB")
        }); /**@todo: handle complex connection options*/
        //mongoose.connection.on('error', console.error.bind(console, 'connection error:'));
    }
    catch(err){
        logger.debug("mngoose connection error", err)
    }
}

function service(nameOpts, proto){
    logger.debug("creating service ", nameOpts, typeof(proto));
    var opts = typeof(nameOpts) == 'string'? {name: nameOpts}: nameOpts;
    if(!opts.name) throw "Service name must be provided.";
    proto = proto || {};
    return angoose.Service.extend(proto, opts );
}
function getMongoose(){
    angoose.mongoose = angoose.mongoose || options.mongoose;
    if(!angoose.mongoose){
        try{
            angoose.mongoose =  module.parent.require('mongoose');
            logger.debug("Required mongoose from parent")
        }
        catch(err){
            logger.debug("No mongoose in parent module");
            angoose.mongoose = require("mongoose");
        }

    }
    return angoose.mongoose;
}
function noop(){};

function hookupExtensions(){
    logger.debug("Searching Extensions in Modules");
    toolbox.removeHookPoints(Pipeline);
    Bundle.initHooks();
    Object.keys(beans).forEach(function(bname){
        var bean = beans[bname];
        if(!bean._angoosemeta.isExtension) return;
        logger.debug("Loading extension", bname);
        bean.config('visibility', false);
        registerHook(bean, bname);
    });
}

function registerHook(middleware, name){
    if(!middleware) return;
    var hooks = require("hooks");
    // if(typeof(middleware) == 'string'){
        // try{
            // if(fs.existsSync(path.resolve(__dirname,  "../extensions/"+ middleware+".js"))){
                // logger.debug("loading built-in extension", middleware);
                // middleware = require("../extensions/"+ middleware);
            // }
            // else
                // middleware = require(middleware);
        // }
        // catch(err){
            // logger.error("Require middleware", middleware, " failed", err);
        // }
    // }
    //if(typeof(middleware)!='object') throw new Error("Angoose middleware is not an object: " + middleware);
    //if(!middleware.name) throw new Error("Middleware must have a name");
    //var name = middleware.name;

    Object.keys(middleware).forEach(function(key){
        if(!middleware.hasOwnProperty(key)) return;
        var func = middleware[key];
        if(typeof (func)!='function') return;

        var hookType = null;
        var method = null;
        if(key.indexOf('pre') == 0){
            hookType = 'pre';
            method =  key.substring('pre'.length);
        }
        else if(key.indexOf('before') == 0){
            hookType = 'pre';
            method =  key.substring('before'.length);
        }
        else if(key.indexOf('post') == 0){
            hookType = 'post';
            method =  key.substring('post'.length);
        }
        else if(key.indexOf('after') == 0){
            hookType = 'post';
            method =  key.substring('after'.length);
        }
        if(!hookType || !method){
            return;
        }
        method = method.substring(0,1).toLowerCase() + method.substring(1);
        if(Pipeline.prototype[method]){
            logger.debug("setting up extension point: ", name, hookType, method)
            Pipeline[hookType](method, func);
        }
        else if(!Bundle[hookType](method, func)){
            logger.error("Unknown extension point: ", key);
        }
    });

    logger.info("Registered  Extension", name);
}

// function registerHooks(){
    // logger.debug("Registering extensions");
    // toolbox.removeHookPoints(Pipeline);
    // toolbox.removeHookPoints(ClientSchema);
//
    // var extensions = options['core-extensions'].concat(   options.hooks || options.extensions || []);
    // if(extensions){
        // var hooks = Array.isArray(extensions)? extensions:[extensions];
        // _.each(hooks, function(hook){
            // // unregister first
            // registerHook(hook);
        // })
    // }
// }
// function reload(module){
    // var name = require.resolve(module);
    // delete require.cache[name];
    // return require(module);
// }

function getLogger(name){
    name = name || 'angoose';
    return require("log4js").getLogger(name);
}

function initialConfig(conf){
    options = _.extend({}, DEFAULT_OPTIONS);

    /** overrite default configurations */
   Object.keys(conf).forEach(function(key){
       toolbox.setter(options, key,  conf[key]); // this way we will allow "forms.url.prefix" type of configuration
   })
    //_.extend(options, conf );
    if(conf && conf.urlPrefix)  options['url-prefix'] = conf.urlPrefix;
    if(conf && conf.modelDir)  options['module-dirs'] = conf.modelDir;
    if(conf && conf.mongo_opts)  options['mongo-opts'] = conf.mongo_opts;
    if(conf && conf.clientFile)  options['client-file'] = conf.clientFile;
    logger.setLevel( options.logging || 'INFO' );

    angoose.initialized = true;
}
/**
 *  Set or get configurations.
 *
 *  Usage:
 *
 *   config()    get the whole configuration object
 *   config(object)    set the configuration by merging the configuration options
 *   config(path)  get the configuration value at path.
 *   config(path, val) set the value at path.
 */
function config(path, val){
    if(!path) return options; /**@todo: probably a deep copy */
    if(!angoose.initialized && typeof(path) == 'string') throw "Cannot call config(" + path+") before angoose is intialized";
    //if(angoose.initialized && typeof(conf) == 'object') throw "Cannot config Angoose after startup";

    if(typeof (path) === 'string'){
         if(val === undefined)
            return toolbox.getter(options, path);
         toolbox.setter(options, path, val);
    }

    if(typeof(path) === 'object'){
        // deep merge
        options = toolbox.merge(options, path);

    }
}

function moduleNames(){
    return Object.keys(beans);
}
function angoose(){
    return lookupOrRegister.apply(null, arguments)
}

function bootstrap(){

    /** statics */
    angoose.Remotable = require("./Remotable");
    angoose.Principal = require("./Principal");
    angoose.Context = require("./Context");
    angoose.Model = require("./Model");
    angoose.Bundle = Bundle;

    angoose.init = init;
    angoose.getClass = getClass;
    angoose.getContext = getContext;
    angoose.testContext = testContext;
    angoose.generateClient = generateClient; /** testing only */
    angoose.service = service;
    angoose.getMongoose = getMongoose;
    angoose.rmiAccept = Pipeline.accept; /** for unit test purpose*/
    angoose.setContext = setContext; // for test
    angoose.execute = Pipeline.execute;

    /** public API */
    angoose.Model = require("./Model");
    angoose.Service = require("./Service");
    angoose.defer = defer;
    angoose.inContext = inContext;
    angoose.config = config;
    angoose.getLogger = getLogger;
    angoose.hasModule = hasModule;
    /**@since 0.2.13 */
    angoose.bind = inContext;
    angoose.client = getClient;

    angoose.moduleNames = moduleNames;

    angoose.module = lookupOrRegister
    angoose.extension = registerExtension;
    angoose.mongoose = getMongoose();
    //getMongoose();
}

bootstrap();

module.exports =  angoose;


/**
 * Module dependencies
 */

var path = require('path')
var vm = require('vm')
var mongo = require('mongodb')
var createDB = require('./db')
var abort = require('./abort')
var getUrl = require('./url')
var debug = require('./debug')
var log = require('./log')
var term = require('./term')
var createContext = require('./context');
var prompt = 'gomon> ';

module.exports = exports = function (program) {
  log('\033[36mgomon\033[0m version %s', require('../package').version);

  var address;
  var files = [];

  program.args.forEach(function (arg) {
    if (/\.js$/.test(arg)) {
      files.push(arg)
    } else {
      address = arg;
    }
  })

  var url = getUrl(address, program);

  connect(url, function (err, db) {
    if (err) return abort(err);
    debug('connected!');
    startShell(createDB(db), program, files);
  })
}

/**
 * Creates a connection to the given `url`.
 */

function connect (url, next) {
  log('connecting to %s', url);
  mongo.Db.connect(url, { db: { w: 1 }}, next);
}

/**
 * Starts up a shell with the given context.
 */

function startShell (db, program, files) {
  var repl = global.repl = term(db);

  createContext(db, repl, function () {
    var code = program.eval;

    if (code) {
      executeJS(code);
      if (!program.shell) {
        repl.emit('exit');
        return;
      }
    }

    if (files.length) {
      executeFiles(files);
      printCloseMsg();
    }

    repl.prompt = prompt;
    repl.displayPrompt()
  });
}

/**
 * Tell users how to shut down script execution
 */

function printCloseMsg () {
  log('------------------------------------------');
  log('  waiting for script to call `exit;` ...');
  log('  OR press Ctrl+D to quit immediately');
  log('------------------------------------------');
}

/**
 * Execute the given `files` in the context shared
 * with the shell when started.
 */

function executeFiles (files) {
  var dir = process.cwd();
  files.forEach(function (file) {
    require(dir + '/' + file);
  });
}

/**
 * Executes javascript passed by --eval
 *
 * @param {String} script to evaluate
 */

function executeJS (script) {
  if (!script) return;

  try {
    var ret = vm.runInThisContext(script, '[eval]');
    if ('undefined' != typeof ret) {
      console.log(ret);
    }
  } catch (err) {
    if (!(err instanceof Error)) {
      err = new Error(err);
    }
    console.log(err.stack.split('\n')[0]);
    process.exit(1);
  }
}


//@flow

const noFallback = (function featureDetect() {
  try {
    return typeof Error.captureStackTrace === 'function'
  } catch (error) {
    return false
  }
})()

/*::
const callAssign = {
  call(thisFn: any, arg: string){

  }
}

class ExtendableErrorClass extends Error {}
*/

function ErrorFunction(message: string) {
  const Err = /*::Object.assign({}, callAssign, */Error/*::)*/
  Err.call(this, message)
  if (noFallback) {
    Error.captureStackTrace(this, ErrorFunction)
  } else {
    this.stack = (new Error()).stack
  }
  this.name = this.constructor.name
  this.message = message
}

ErrorFunction.prototype = Object.create(Error.prototype)
ErrorFunction.prototype.constructor = ErrorFunction

const ExtendableError: Class<ExtendableErrorClass> = (ErrorFunction: any)

export default ExtendableError


/**
 * Dependencies
 */

var mongodb = require('mongodb')
var slice = require('sliced')
var log = require('./log')
var p = require('./p')
var noop = function(){}

/**
 * Expose
 */

module.exports = exports = create;

function create (db) {
  var DB = createConstructor(db);
  return new DB;
}

/**
 * We create DB constructors dynamically to avoid
 * assigning the db or collections directly to the
 * database instance, thereby avoiding them displaying
 * in autocomplete mixed together with the collection
 * names.
 *
 * @param {Database} db
 * @return {Function}
 */

function createConstructor (db) {
  var collections = [];

  function DB () {
    this.cols(false, noop);
  }

  /**
   * Logs help text for all db methods
   */

  ;(DB.prototype.help = function () {
    var proto = this.constructor.prototype;
    var len = 0;
    var msgs = [];
    Object.keys(proto).forEach(function (method) {
      if (proto[method].help) {
        var msg = proto[method].help(true);
        len = Math.max(len, method.length);
        msgs.push({ method: method, text: msg })
      }
    })
    msgs.sort(function (a, b) {
      return a.method > b.method ?  1 :
             a.method < b.method ? -1 :
             0
    })
    msgs.forEach(function (msg) {
      var space =  Array(len - msg.method.length +1).join(' ');
      log("db." + msg.method + "() " + space + msg.text);
    })
  }).help = help("Logs help text for all db methods");

  /**
   * Drop this database
   *
   * @param {Function} [cb]
   */

  ;(DB.prototype.drop = function (cb) {
    var name = db.databaseName;

    db.dropDatabase(cb || function (err) {
      if (err) {
        return console.error(err);
      }

      db.collections(function () {
        log('database "%s" was dropped', name);
      });
    });
  }).help = help("Drops the database");
  wrap(DB.prototype, 'drop');

  /**
   * Close this database connection
   *
   * @param {Function} [cb]
   */

  ;(DB.prototype.close = function (cb) {
    if ('function' != typeof cb) {
      cb = handleError;
    }

    db.close(true, cb);
  }).help = help("Closes the database connection");
  wrap(DB.prototype, 'close');

  /**
   * Open the database connection
   *
   * @param {Function} [cb]
   */

  ;(DB.prototype.open = function (cb) {
    db.open(function (err) {
      if (err) {
        return handleError(err, cb);
      }

      if ('function' == typeof cb) {
        cb();
      }
    });
  }).help = help("Opens the database connection")
  wrap(DB.prototype, 'open');

  /**
   * Use a different database
   */

  ;(DB.prototype.use = function (name) {
    return create(db.db(name));
  }).help = help("Changes to a different database")

  /**
   * Access a collection
   */

  ;(DB.prototype.col = function (name, opts) {
    if (this[name]) {
      return this[name];
    }

    // accessor management
    collections.push(name);

    return this[name] = db.collection(name, opts);
  }).help = help("Accesses a collection")

  /**
   * Creates a collection
   *
   * @param {String} name
   * @param {Object} [options]
   * @param {Function} [cb]
   */

  ;(DB.prototype.createCol = function (name, opts, cb) {
    if ('function' == typeof opts) {
      cb = opts;
      opts = {};
    }

    if ('string' != typeof name) {
      error(new TypeError('collection name must be a string'));
      return;
    }

    if (!opts) opts = {};

    // force error if collection exists
    if (!('strict' in opts)) opts.strict = true;

    var self = this;
    db.createCollection(name, opts, function (err, col) {
      if (err) {
        if (/already exists/.test(err)) {
          // remove the "safe mode" message
          err.message = 'collection "' + name + '" already exists';
        }
        return handleError(err);
      }

      // register name for accessor management
      collections.push(name);

      return self[name] = col;
    });

  }).help = help("Creates a collection")
  wrap(DB.prototype, 'createCol');

  /**
   * Refresh and return the list of collections on this database
   *
   * @param {Boolean} [print] if the collection names should be printed
   * @param {Function} [cb] passed any error and the result array
   */

  ;(DB.prototype.cols = function (print, cb) {
    var self = this;

    if ('function' == typeof print) {
      cb = print;
      print = false;
    }

    if (undefined == print) print = true

    db.collectionNames({ namesOnly: true }, function (err, names) {
      if (err) {
        if (cb) return cb(err);
        console.error(err.stack);
        return;
      }

      if (!Array.isArray(names)) {
        names = [];
      }

      // remove cached collections
      collections.forEach(function (name) {
        delete self[name];
      });

      // strip db from name
      var ns = db.databaseName;
      var len = ns.length + 1;
      names = names.map(function (name) {
        return name.substring(len);
      });

      collections = names;

      // expose collection access from `db`
      // TODO abstract collection
      names.forEach(function (name) {
        self[name] = db.collection(name);

        // handle system.indexes etc
        if (/\./.test(name)) {
          var parts = name.split('.');
          parts.reduce(function (out, part, i) {
            if (i == parts.length - 1) {
              out[part] = self[name];
            } else {
              if (!out[part]) {
                out[part] = {};
              }
            }
            return out[part];
          }, self);
        }
      });

      if (cb) return cb(err, names);

      if (print) {
        console.log();
        names.forEach(function (name) {
          log(name);
        })
        global.repl.displayPrompt();
      }
    });
  }).help = help("Retreives an array of collection names in the db")
  wrap(DB.prototype, 'cols');

  /**
   * Execute a command on the database
   *
   * @param {Object} cmd
   * @param {Object} [opts]
   * @param {Function} [cb]
   */

  ;(DB.prototype.runCommand = function (cmd, opts, cb) {
    if ('function' == typeof opts) {
      cb = opts;
      opts = {};
    }

    if (!cmd) {
      var err = new Error('missing command');
      if (cb) return cb(err);
      console.error(err);
      return;
    }

    if (!cb) cb = p;
    if (!opts) opts = {};

    var admin = !! opts.admin;
    delete opts.admin;

    var method = admin
      ? 'executeDbAdminCommand'
      : 'executeDbCommand'

    db[method](cmd, opts, cb);
  }).help = help("Runs a command on the database")
  wrap(DB.prototype, 'runCommand');

  /**
   * Retreive database stats
   */

  ;(DB.prototype.stats = function (scale, cb) {
    if ('function' == typeof scale) cb = scale;
    scale |= 0;

    db.stats(function (err, stats) {
      cb(err, stats);
    })
  }).help = help('Retreive database stats');
  wrap(DB.prototype, 'stats');

  /**
   * console.log helper
   */

  ;(DB.prototype.inspect = function () {
    return db.databaseName;
  }).help = help("Returns the name of the database");

  return DB;

}

/**
 * Wrap async functions with animation etc
 */

function wrap (proto, name) {
  var old = proto[name];

  proto[name] = function () {
    if (global.repl) global.repl.bufferStart();
    var args = slice(arguments);
    var last = args[args.length-1];
    if ('function' == typeof last) {
      args[args.length-1] = function () {
        if (global.repl) global.repl.bufferEnd()
        if (p != last) console.log();
        last.apply(null, arguments)
        if (global.repl) {
          global.repl.displayPrompt();
          global.repl.moveCursorToEnd();
        }
      }
    } else {
      args.push(function () {
        if (global.repl) global.repl.bufferEnd()
        p.apply(null, arguments);
        if (global.repl) global.repl.moveCursorToEnd();
      });
    }
    old.apply(this, args);
  }

  if (old.help) {
    proto[name].help = old.help;
  }
}

/**
 * Error reporting helper
 */

function handleError (err, cb) {
  if (err) {
    if (cb) {
      return process.nextTick(function(){
        cb(err);
      });
    }
    console.error(err);
  }
}

/**
 * help genereator
 */

function help (msg) {
  return function (inline) {
    if (inline) return msg;
    log(msg);
  }
}

/**
 * use.database
 *
 * [x] db.drop()
 * [ ] db.clone() // cloneDatabase()
 * [ ] db.copy()  // copyDatabase()
 * [x] db.open()
 * [x] db.close()
 * [x] db.use()
 * [x] db.help()  // make combination of help() & db.listCommands()
 * [ ] db.eval()
 * [x] db.createCollection()
 * [x] db.collectionNames() // getCollectionNames
 * [ ] db.collection.find()
 *
 * [ ] db.addUser()
 * [ ] db.removeUser()
 * [ ] db.auth()
 * [ ] db.logout()
 * [ ] db.changeUserPassword()
 *
 * [x] db.runCommand()
 * [~] db.adminCommand()
 *
 * [ ] db.repair() // db.repairDatabase()
 *
 * [ ] db.stats()
 * [ ] db.profilingLevel() // with args is a setter, else getter
 * [ ] db.readPref()
 *
 * [ ] db.loadServerScripts() // from system.js collection into the shell
 *
 * // belongs on a server object
 * [ ] server.isMaster()
 * [ ] .status()    // .serverStatus()
 * [ ] .shutdown()  // .shutdownServer()
 * [ ] .hostInfo()
 * [ ] .version()
 * [ ] .cmdLineOpts // .serverCmdLineOpts()
 * [ ] .killOp()
 * [ ] .currentOp() * // should filter by this db
 * [ ] .fsyncLock()
 * [ ] .fsyncUnlock()
 *
 * // rs
 * [ ] .status()
 * [?] .replicationInfo()
 * [?] .slaveReplicationInfo()
 *
 * // sh
 * [ ] .status()
 *
 * // unclear
 * [👎 ] db.getLastErrorCmd()
 * [👎 ] db.getLastErrorObj()
 * [👎 ] db.getPrevError()
 * [👎 ] db.getLastError()
 *
 * // belongs on collections
 * [ ] .cloneCollection()
 * [ ] .currentOp() * filtered
 */


const heading = (h) => {
  const value = `\x1b[1m${h}\x1b[0m`
  return { value, length: h.length }
}

const getLength = (value) => {
  return value.replace(/\033\[.*?m/g, '').length
}

const makeBinaryHash = (arr) => {
  const res = arr.reduce((acc, current) => {
    return {
      ...acc,
      [current]: true,
    }
  }, {})
  return res
}

/**
 * Display values in a table.
 * @param {Config} conf Options for the program.
 * @param {Array<string>} conf.keys Keys to print as columns.
 * @param {Array<Object<string, string>>} conf.data The array of data items to prints as rows.
 * @param {Object<string, string>} [conf.headings] The display names for each column.
 * @param {Object.<string, Replacement>} [conf.replacements] The map of replacement functions which will run against data items.
 * @param {Array<string>} [conf.centerValues] Centre values of these column (use original keys, not headings).
 * @param {Array<string>} [conf.centerHeadings] Center headings of these column (use original keys, not headings).
 * @returns {string} A string which represents a table.
 */
               function tablature(conf) {
  const {
    keys = [],
    data = [],
    headings = {},
    replacements = {},
    centerValues = [],
    centerHeadings = [],
  } = conf
  const [i] = data
  if (!i) return ''

  const cv = makeBinaryHash(centerValues)
  const hv = makeBinaryHash(centerHeadings)

  const k = Object.keys(i).reduce((acc, key) => {
    const h = headings[key]
    return {
      ...acc,
      [key]: h ? h.length : key.length, // initialise with titles lengths
    }
  }, {})

  const widths = data.reduce((dac, d) => {
    const res = Object.keys(d).reduce((acc, key) => {
      const maxLength = dac[key]
      const val = d[key]
      const r = getReplacement(replacements, key)
      const { length } = r(val)
      return {
        ...acc,
        [key]: Math.max(length, maxLength),
      }
    }, {})
    return res
  }, k)

  const kk = keys.reduce((acc, key) => {
    const h = headings[key]
    return {
      ...acc,
      [key]: h || key,
    }
  }, {})
  const hr = keys.reduce((acc, key) => {
    return {
      ...acc,
      [key]: heading,
    }
  }, {})
  const hl = getLine(keys, kk, widths, hr, hv)
  const rl = data.map((row) => {
    const line = getLine(keys, row, widths, replacements, cv)
    return line
  })
  return [
    hl,
    ...rl,
  ].join('\n')
}

const pad = (val, length, replacement, cen) => {
  if (val === undefined) return ' '.repeat(length)
  let v = val
  let l
  if (replacement) {
    const { value, length: len } = replacement(val)
    v = value
    l = len
  } else {
    l = `${val}`.length
  }
  const p = length - l
  if (cen) {
    const left = Math.floor(p / 2)
    const right = p - left
    const s = ' '.repeat(left) + v + ' '.repeat(right)
    return s
  }
  const s = ' '.repeat(p)
  return `${v}${s}`
}

const getReplacement = (replacements, key) => {
  const r = replacements[key]
  if (!r) return value => ({
    value,
    length: getLength(value),
  })
  return r
}

const getLine = (keys, values, widths, replacements = {}, center = {}) => {
  let currentWidth = 0
  const k = keys.map(key => {
    const w = widths[key]
    if (!w) throw new Error(`Unknown field ${key}`)
    const v = values[key]
    const r = getReplacement(replacements, key)
    const cen = center[key]
    const [rh, ...rows] = v.split('\n')
    const h = pad(rh, w, r, cen)
    let rs = ''
    if (rows.length) {
      rs = '\n' + rows.map(row => {
        const wb = ' '.repeat(currentWidth)
        const rv = pad(row, w, r, cen)
        return `${wb}${rv}`
      }).join('\n')
    }
    const res = `${h}${rs}`
    currentWidth += w + 2
    return res
  })
  const line = k.join('  ')
  return line
}

/* documentary types/index.xml */
/**
 * @typedef {(value: string) => {value: string, length: number}} Replacement The function to use to replace values for display.
 *
 * @typedef {Object} Config Options for the program.
 * @prop {Array<string>} keys Keys to print as columns.
 * @prop {Array<Object<string, string>>} data The array of data items to prints as rows.
 * @prop {Object<string, string>} [headings] The display names for each column.
 * @prop {Object.<string, Replacement>} [replacements] The map of replacement functions which will run against data items.
 * @prop {Array<string>} [centerValues] Centre values of these column (use original keys, not headings).
 * @prop {Array<string>} [centerHeadings] Center headings of these column (use original keys, not headings).
 */

module.exports = tablature


var _ = require('lodash');
var moment = require('moment');
var Logger = require('logger-facade-nodejs');
var log = Logger.getLogger('micro.metric.client');

var microHeaders = [
  'micro.cs', 'micro.cr',
  'micro.ss', 'micro.sr',
  'micro.bfes', 'micro.bfer',
  'micro.bbes', 'micro.bber'
];

// Add Test: Add proper test to ensure the format of the metric, mocking logger was giving troubles
function metric(name, val, message) {
  // val is not a NaN should log the metric
  if (isNaN(val)) { return message; }

  var metadata = _.pick(message, ['type', 'rid', 'address', 'status', 'client', 'clientId', 'transaction']);
  metadata['micrometric'] = { name: name, value: val }
  log.info(metadata, 'Publishing metric "%s":%sms', name, val);
  return metadata;
}

function start(message) {
  var now = moment().valueOf();
  var headers = message.headers || {};
  var stack = headers['micro-metrics-stack'] || [];

  // since a new request span is created need to persist
  // the micro headers stack accross calls of several
  // services when present.
  var callStackHeaders = _.pick(headers, microHeaders);
  if (Object.keys(callStackHeaders).length) {
    stack.push(callStackHeaders);
  }

  // omit the micro metric headers and the old stack
  var headers = _.omit(headers, microHeaders, 'micro-metrics-stack');
  // add the micro client send timestamp
  message.headers = _.defaults({},
    headers,
    {'micro.cs': now}
  );

  // only include stack if is not empty
  if (stack.length) {
    message.headers['micro-metrics-stack'] = stack;
  }

  metric('micro.cs', now, message);
  return message;
}

function end(message) {
  var now = moment().valueOf();

  // remove the headers, from the current call stack
  // and restore the micro headers from call stack
  // and add the micro client receive timestamp
  var callStackHeaders = message.headers || {};
  var oldStackHeaders = callStackHeaders['micro-metrics-stack'] || [];
  message.headers = _.defaults({},
    _.omit(message.headers, microHeaders, 'micro-metrics-stack'),
    {'micro.cr': now},
    oldStackHeaders.pop());

  metric('micro.cr', now, message);

  var cs = callStackHeaders['micro.cs'];
  metric('micro.c.span', now - cs, message);

  var bfes = callStackHeaders['micro.bfes'];
  metric('micro.bc.span', now - bfes, message);

  return message;
}

module.exports = {start: start, end: end};


/*
 * StdError
 *
 */

module.exports = StdError;

//==============================================================================
//-- constructor

function StdError(message) {
  if (! (this instanceof StdError)) {
    return new StdError(message);
  }

  Error.captureStackTrace(this, this.constructor);

  this.code = this._defaults.code;
  this.name = this._defaults.name;
  this.message = this._defaults.message;

  switch (typeof(message)) {
    case "string":
      this.message = message;
      break;
    case "object":
      (message.code) && (this.code = message.code);
      (message.name) && (this.name = message.name);
      (message.message) && (this.message = message.message);
      break;
  }
}

//------------------------------------------------------------------------------

StdError.prototype = Object.create(Error.prototype, {
  constructor: {
    value: StdError
  },
  _defaults: {
    value: {
      code: null,
      name: "StdError",
      message: "Standard Error"
    }
  }
});

//------------------------------------------------------------------------------

StdError.prototype.toString = function() {
  var str = this.name;

  if (this.name !== this.message) {
    str += ": " + this.message;
  }

  return str;
};

//------------------------------------------------------------------------------

Object.defineProperty(StdError, "_name", { value: StdError.name });

//------------------------------------------------------------------------------

Object.defineProperty(StdError, "extend", {
  value: function(options) {
    var self = this;
    var defaults = {
      code: null,
      name: "",
      message: ""
    };

    switch (typeof(options)) {
      case "string":
        defaults.name = defaults.message = options;
        break;
      case "object":
        (options.code) && (defaults.code = options.code);
        (options.name) && (defaults.name = options.name);
        defaults.message = options.message || defaults.name;
        break;
    }

    switch (defaults.name) {
      case "extend":
      case "define":
      case "_name":
      case "":
        throw new Error("invalid name");
        break;
    }

    //-- constructor
    var child = function() {
      var args = Array.prototype.slice.call(arguments);

      if (! (this instanceof child)) {
        var obj = Object.create(child.prototype);
        child.apply(obj, args);
        return obj;
      }

      self.apply(this, args);
    };

    child.prototype = Object.create(self.prototype, {
      constructor: { value: child },
      _defaults: { value: defaults }
    });
    Object.defineProperties(child, {
      _name:  { value: defaults.name },
      extend: { value: self.extend },
      define: { value: self.define }
    });

    return child;
  }
});

//------------------------------------------------------------------------------

Object.defineProperty(StdError, "define", {
  value: function(options) {
    var self = this;
    var E = self[(typeof(options) === "object") ? options.parent : ""];
    var error = (E || self).extend(options);

    if (error) {
      Object.defineProperty(self, error._name, { value: error });
    }

    return error;
  }
});

//==============================================================================


/*jshint strict:true node:true es5:true onevar:true laxcomma:true
laxbreak:true eqeqeq:true immed:true latedef:true undef:true unused:true */
(function () {
  "use strict";

  var connect = require('connect')
    , fs = require('fs')
    , path = require('path')
    ;

  function defaultPermalinkify(oldUrl) {
    // get everything except the domain
    return oldUrl.replace(/https?:\/\/[^\/]*/, '');
  }

  function createServer(pub, servepath, opts) {
    opts = opts || {};
    var server
      , permalinkify
      ;

    function fuzzyRedirect(request, response, next) {
      //console.log('fuzzyRedir', request.url);
      var pathPart = request.url.split('/')
        , requestedFile = request.url.split('/').pop()
        , realDirPath
        ;

      pathPart.pop();
      pathPart = pathPart.join('/');

      //console.log('pathPart', pathPart);
      function sanatize(str) {
        return str
          .replace(/\.\w+$/, '')     // remove .html
          .replace(/%[\dA-Z]+/g, '') // remove %20
          .replace(/\ban?\b/g, '')   // ignore a/an
          .replace(/\bthe\b/g, '')   // ignore the
          .replace(/\bof\b/g, '')    // ignore of
          .replace(/[^a-z0-9]/g, '') // remove non-alphanumerics
          ;
      }

      function alphabetSoup(existingFile) {
        var shorter = sanatize(existingFile)
          , longer = sanatize(requestedFile.split('/').pop())
          , len = Math.min(25, shorter.length, longer.length) - 1
          ;

        if (existingFile === requestedFile) {
          // just in case connect.static() is being loaded late
          return;
        }

        if (shorter.substr(0, len) !== longer.substr(0, len)) {
          // console.log(shorter, longer);
          return;
        }

        response.statusCode = 301;
        response.setHeader(
            'Location'
          , path.normalize(servepath + '/' + pathPart + '/' + existingFile)
        );
        response.end();
        return true;
      }

      function fuzzy301(err, nodes) {
        if (err) { next(); return; }
        if (!nodes.some(alphabetSoup)) {
          next();
        }
      }

      // joining /rootdir, /sub/dir/ -> /rootdir/sub/dir/
      realDirPath = path.join(pub, servepath.replace(/[\/\\]/g, '/'), pathPart);

      //console.log('searchdir', realDirPath);
      fs.readdir(
          realDirPath
        , fuzzy301
      );
    }

    function solidRedirect(request, response, next) {
      //console.log('fixRedir', request.url);

      function fixRedirectPost() {
        // Permament Redirect
        response.statusCode = 301;
        response.setHeader(
            'Location'
          , path.normalize(servepath + '/' + permalinkify(request.query.blogger))
        );
        response.end();
      }

      if (request.query.blogger) {
        fixRedirectPost();
        return;
      }

      function fixDoubleDash() {
        // Permament Redirect
        response.statusCode = 301;
        response.setHeader('Location', request.url.replace(/-+/g, '-'));
        response.end();
      }

      if (/--/.test(request.url)) {
        fixDoubleDash();
        return;
      }

      next();
    }

    permalinkify = opts.permalinkify || defaultPermalinkify;
    servepath = '/' + (servepath || '').replace(/[\/\\]/g, '');

    server = connect.createServer()
      .use(connect.query())
      .use(solidRedirect)
      // fuzzy redirect
      .use(path.normalize('/' + servepath), fuzzyRedirect)
      ;

    return server;
  }

  module.exports = createServer;
}());


'use strict';

var lodash = require('lodash');
var isPromise = require('is-promise');

// Returns a lodash chain that calls .value()
// automatically after the first .method()
//
// It also returns a promise or value
//
// For example:
// lowChain(_, array, save).method()
//
// is the same as:
// _.chain(array).method().value()
function lowChain(_, array, save) {
  var chain = _.chain(array);

  _.functionsIn(chain).forEach(function (method) {
    chain[method] = _.flow(chain[method], function (arg) {
      var v = void 0;
      if (arg) {
        v = _.isFunction(arg.value) ? arg.value() : arg;
      }

      var s = save();

      if (s) return s.then(function () {
        return Promise.resolve(v);
      });
      return v;
    });
  });

  return chain;
}

function low(source) {
  var options = arguments.length <= 1 || arguments[1] === undefined ? {} : arguments[1];
  var writeOnChange = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2];

  // Create a fresh copy of lodash
  var _ = lodash.runInContext();

  if (source) {
    if (options.storage) {
      (function () {
        var storage = options.storage;

        if (storage.read) {
          db.read = function () {
            var s = arguments.length <= 0 || arguments[0] === undefined ? source : arguments[0];

            var res = storage.read(s, db.deserialize);

            if (isPromise(res)) {
              return res.then(function (obj) {
                db.object = obj;
                db._checksum = JSON.stringify(db.object);

                return db;
              });
            }

            db.object = res;
            db._checksum = JSON.stringify(db.object);

            return db;
          };
        }

        if (storage.write) {
          db.write = function () {
            var dest = arguments.length <= 0 || arguments[0] === undefined ? source : arguments[0];
            return storage.write(dest, db.object, db.serialize);
          };
        }
      })();
    }

    if (options.format) {
      var format = options.format;

      db.serialize = format.serialize;
      db.deserialize = format.deserialize;
    }
  }

  // Modify value function to call save before returning result
  _.prototype.value = _.wrap(_.prototype.value, function (value) {
    var v = value.apply(this);
    var s = _save();

    if (s) return s.then(function () {
      return Promise.resolve(v);
    });
    return v;
  });

  // Return a promise or nothing in sync mode or if the database hasn't changed
  function _save() {
    if (db.source && db.write && writeOnChange) {
      var str = JSON.stringify(db.object);

      if (str !== db._checksum) {
        db._checksum = str;
        return db.write(db.source, db.object);
      }
    }
  }

  function db(key) {
    if (typeof db.object[key] === 'undefined') {
      db.object[key] = [];
    }
    var array = db.object[key];
    var short = lowChain(_, array, _save);
    short.chain = function () {
      return _.chain(array);
    };
    // Prevents db.write being called when just calling db('foo').value()
    short.value = function () {
      return db.object[key];
    };
    return short;
  }

  // Expose
  db._ = _;
  db.object = {};
  db.source = source;

  // Init
  if (db.read) {
    return db.read();
  } else {
    return db;
  }
}

module.exports = low;


/***
 * Copyright (c) 2015 - 2019 Alex Grant (@localnerve), LocalNerve LLC
 * Licensed under the MIT license.
 *
 * Recursive file, line picker and text replacer.
 * Use to replace specific text across file types in a project.
 */
/* global require, module, console */
/* eslint-disable no-console */
'use strict';

const fs = require('fs');
const path = require('path');
const picklrActions = require('./actions');

/**
 * The Picklr object.
 *
 * @constructor
 */
function Picklr (startDir, options) {
  options = options || {};

  let defaultExcludeDirsRe;
  if (/^\./.test(startDir)) {
    defaultExcludeDirsRe = /\/\.|node_modules/i;
  } else {
    defaultExcludeDirsRe = /^\.|\/\.|node_modules/i;
  }

  this.totalFileCount = 0;
  this.matchedFileCount = 0;
  this.startDir = startDir || '.';
  this.targetText = options.targetText || '';
  this.replacementText = options.replacementText || '';
  this.action = options.action || 'echo';
  this.includeExts = options.includeExts || ['.js'];
  this.excludeDirs = options.excludeDirsRe || defaultExcludeDirsRe;
  this.logger = options.logger || console.log;

  this.picklrActions = picklrActions;
}
Picklr.prototype = {
  /**
   * Recursively process files.
   */
  recurseFiles: function (p) {
    fs.readdirSync(p).forEach(function (file) {
      const curPath = path.join(p, path.sep, file);
      const stats = fs.statSync(curPath);

      if (this.isDirectory(stats, curPath)) {
        this.recurseFiles(curPath);
      } else if (this.isFile(stats, curPath)) {
        this.picklrActions[this.action].call(this, curPath);
      }
    }, this);

    if (p === this.startDir) {
      this.logger('Total file count = ' + this.totalFileCount);
      if (this.action !== 'echo') {
        this.logger('Matched file count = ' + this.matchedFileCount);
      }
    }
  },
  /**
   * File type determination.
   *
   * @private
   * @returns true if given path is a file.
   */
  isFile: function (stats, p) {
    let result = stats.isFile();

    if (result) {
      const ext = path.extname(p);
      result = this.includeExts.indexOf(ext) !== -1;
    }

    return result;
  },
  /**
   * Directory type determination.
   *
   * @private
   * @returns true if given path is a directory.
   */
  isDirectory: function (stats, p) {
    let result = stats.isDirectory();

    if (result) {
      result = !this.excludeDirs.test(p);
    }

    return result;
  }
};

/**
 * Process all files under startDir using options.
 */
function processAllFiles (startDir, options) {
  const picklr = new Picklr(startDir, options);
  picklr.recurseFiles(startDir);
}

module.exports = processAllFiles;


/***
 * Copyright (c) 2015 - 2019 Alex Grant (@localnerve), LocalNerve LLC
 * Licensed under the MIT license.
 *
 * Picklr actions class.
 */
/* global require, module */
'use strict';

const fs = require('fs');

/**
 * Does the actual audit or update of a single file.
 *
 * @private
 */
function processFile (filePath, update) {
  let change, found = false;
  const lines = fs.readFileSync(filePath, { encoding: 'utf8' }).split('\n');

  for (let i = 0; i < lines.length; i++) {
    if (lines[i].indexOf(this.targetText) !== -1) {
      found = true;
      change = lines[i].replace(
        this.targetText, this.replacementText
      );
      if (update) {
        lines[i] = change;
      } else {
        // log the line that would be edited.
        this.logger('*** File:   ' + filePath);
        this.logger('@@@ Found:  ' + lines[i]);
        this.logger('--- Change: ' + change);
      }
      this.matchedFileCount++;
      break;
    }
  }

  if (!found && !update) {
    // log the file that would be omitted
    this.logger('*** Omitted: ' + filePath);
  }

  if (found && update) {
    fs.writeFileSync(filePath, lines.join('\n'), { encoding: 'utf8' });
    // log the updated file
    this.logger('@@@ Updated: ' + filePath);
  }

  this.totalFileCount++;
}

module.exports = {
  /**
   * Print out the set of affected files.
   */
  echo: function (filePath) {
    this.logger(filePath);
    this.totalFileCount++;
  },
  /**
   * Audit the set of affected files.
   */
  audit: function (filePath) {
    processFile.call(this, filePath, false);
  },
  /**
   * Update the affeted files.
   */
  update: function (filePath) {
    processFile.call(this, filePath, true);
  }
};


/*
 * Copyright 2011 jolira
 *
 * Licensed under the Apache License, Version 2.0 (the "License"); you may not
 * use this file except in compliance with the License. You may obtain a copy of
 * the License at http://www.apache.org/licenses/LICENSE-2.0. Unless required by
 * applicable law or agreed to in writing, software distributed under the
 * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS
 * OF ANY KIND, either express or implied. See the License for the specific
 * language governing permissions and limitations under the License.
 */

var http = require('http');
var https = require('https');
var url = require('url');
var _ = require("underscore");

// Private
// ----------------------------------------------------------------------------

function extractEncoding(headers) {
    var type = headers["content-type"];

    if (!type) {
        return "utf8";
    }

    var split = type.split('=');

    return split.length == 2 ? split[1] : "utf8";
}

function requestJSON(url, content, callback) {
    var client = url.protocol == 'https:' ? https : http;
    var path = url.pathname;

    if (url.search) {
        path += url.search;
    }

    var request = client.request({
        method : "POST",
        host : url.hostname,
        port : url.port,
        path : path
    }, function(response) {
        if (response.statusCode != 200) {
            callback(undefined, response);
            return;
        }

        var body = "";
        var endcoding = extractEncoding(response.headers);

        response.setEncoding(endcoding);
        response.on('data', function(chunk) {
            body += chunk;
        });
        response.on('end', function() {
            var parsed = JSON.parse(body);

            callback(parsed);
        });
    });

    content["config"] = {
        "maxObjects" : 100000,
        "maxCollectionSize" : 10000,
        "ignoreErrors" : true,
        "maxDepth" : 100
    };
    var _json = JSON.stringify(content);

    request.on('error', function(response) {
        callback(undefined, response);
    });
    request.write(_json);
    request.end();
}

function doList(url, mbean, callback) {
    requestJSON(url, {
        "type" : "LIST",
        "path" : mbean
    }, function(response) {
        callback(response);
    });
}

function collectMBeans(json, alreadyFound, scope) {
    var attr = json["attr"];

    if (attr) {
        alreadyFound.unshift(scope);

        return alreadyFound;
    }

    for ( var name in json) {
        if (name === "op" || name == "desc") {
            continue;
        }

        var _scope = scope ? scope + ":" + name : name;
        var _json = json[name];

        collectMBeans(_json, alreadyFound, _scope);
    }

    return alreadyFound;
}

// Public
// ----------------------------------------------------------------------------

function Jolokia(target) {
    this.url = url.parse(target, false);
}

Jolokia.prototype.list = function(arg1, arg2) {
    var mbeans = arg2 ? arg1 : undefined;
    var callback = arg2 ? arg2 : arg1;

    if (!_.isArray(mbeans)) {
        doList(this.url, mbeans, callback);
        return;
    }

    if (mbeans.length < 1) {
        doList(this.url, undefined, callback);
        return;
    }

    var result = {};

    for ( var idx in mbeans) {
        var mbean = mbeans[idx];

        doList(this.url, mbean, function(mbean) {
            return function(json) {
                result[mbean] = json;

                var keys = _.keys(result);

                if (keys.length === mbeans.length) {
                    callback(result);
                }
            };
        }(mbean));
    }
};

Jolokia.prototype.read = function(mbean, arg2, arg3) {
    var attribute = arg3 ? arg2 : undefined;
    var callback = arg3 ? arg3 : arg2;

    if (!_.isArray(mbean)) {
        requestJSON(this.url, {
            "type" : "read",
            "mbean" : mbean,
            "attribute" : attribute
        }, function(response) {
            callback(response);
        });
        return;
    }

    var content = [];

    for ( var idx in mbean) {
        var bean = mbean[idx];

        content[idx] = _.isString(bean) ? {
            "type" : "read",
            "mbean" : bean,
            "attribute" : attribute
        } : {
            "type" : "read",
            "mbean" : bean.mbean,
            "attribute" : bean.attribute ? bean.attribute : attribute
        };
    }

    requestJSON(this.url, content, function(response) {
        callback(response);
    });
};

Jolokia.prototype.dump = function(mbean, callback) {
    var self = this;

    this.list(mbean, function(json) {
        var mbeans = collectMBeans(json.value, [], mbean);

        self.read(mbeans, callback);
    });
};

// Exports
// ----------------------------------------------------------------------------

module.exports = Jolokia;