"use strict";
/**
 * @module template/publish
 * @type {*}
 */
/*global env: true */
var template = require( 'jsdoc/template' ),
	fs = require( 'jsdoc/fs' ),
	_ = require( 'underscore' ),
	path = require( 'jsdoc/path' ),
	taffy = require( 'taffydb' ).taffy,
	handle = require( 'jsdoc/util/error' ).handle,
	helper = require( 'jsdoc/util/templateHelper' ),
	htmlsafe = helper.htmlsafe,
	linkto = helper.linkto,
	resolveAuthorLinks = helper.resolveAuthorLinks,
	scopeToPunc = helper.scopeToPunc,
	hasOwnProp = Object.prototype.hasOwnProperty,
	conf = env.conf.tsdoc || {},
	data,
	view,
	outdir = env.opts.destination;

var globalUrl = helper.getUniqueFilename( 'global' );
var indexUrl = helper.getUniqueFilename( 'index' );

var navOptions = {
	systemName      : conf.systemName || "Documentation",
	navType         : "vertical",
	footer          : conf.footer || "",
	copyright       : conf.copyright || "",
	theme           : "cerulean",
	linenums        : false,
	collapseSymbols : true,
	inverseNav      : false
};

/**
 * Check whether a symbol is a function and is the only symbol exported by a module (as in
 * `module.exports = function() {};`).
 *
 * @private
 * @param {module:jsdoc/doclet.Doclet} doclet - The doclet for the symbol.
 * @return {boolean} `true` if the symbol is a function and is the only symbol exported by a module;
 * otherwise, `false`.
 */
function isModuleFunction2(doclet) {
	return doclet.longname && doclet.longname === doclet.name &&
		doclet.longname.indexOf('module:') === 0 && doclet.kind === 'function';
}

/**
 * Retrieve all of the following types of members from a set of doclets:
 *
 * + Classes
 * + Externals
 * + Globals
 * + Mixins
 * + Modules
 * + Namespaces
 * + Events
 * @param {TAFFY} data The TaffyDB database to search.
 * @return {object} An object with `classes`, `externals`, `globals`, `mixins`, `modules`,
 * `events`, and `namespaces` properties. Each property contains an array of objects.
 */
function getMembers2(data) {
	var find = function(data, spec) {
		return data(spec).get();
	};
	var members = {
		classes: find( data, {kind: 'class'} ),
		externals: find( data, {kind: 'external'} ),
		events: find( data, {kind: 'event'} ),
		globals: find(data, {
			kind: ['member', 'function', 'constant', 'typedef'],
			memberof: { isUndefined: true }
		}),
		mixins: find( data, {kind: 'mixin'} ),
		modules: find( data, {kind: 'module'} ),
		namespaces: find( data, {kind: 'namespace'}),
		typedef: find( data, {kind: 'typedef', isTSEnum:{is:true} }),
		callbacks: find( data, {kind: 'typedef', isTSEnum:{isUndefined:true} })

	};

	// functions that are also modules (as in "module.exports = function() {};") are not globals
	members.globals = members.globals.filter(function(doclet) {
		if ( isModuleFunction2(doclet) ) {
			return false;
		}

		return true;
	});

	return members;
};

var navigationMaster = {
	index     : {
		title   : navOptions.systemName,
		link    : indexUrl,
		members : []
	},
	tutorial : {
		title   : "Tutorials",
		link    : helper.getUniqueFilename( "tutorials.list" ),
		members : []
	},
	namespace : {
		title   : "Namespaces",
		link    : helper.getUniqueFilename( "namespaces.list" ),
		members : []
	},
	module    : {
		title   : "Modules",
		link    : helper.getUniqueFilename( "modules.list" ),
		members : []
	},
	interface     : {
		title   : "Interfaces",
		link    : helper.getUniqueFilename( 'classes.list' ),
		members : []
	},
	class     : {
		title   : "Classes",
		link    : helper.getUniqueFilename( 'classes.list' ),
		members : []
	},
	mixin    : {
		title   : "Mixins",
		link    : helper.getUniqueFilename( "mixins.list" ),
		members : []
	},
	event    : {
		title   : "Events",
		link    : helper.getUniqueFilename( "events.list" ),
		members : []
	},
	global    : {
		title   : "Global",
		link    : globalUrl,
		members : []

	},
	external : {
		title   : "Externals",
		link    : helper.getUniqueFilename( "externals.list" ),
		members : []
	},
	typedef : {
		title   : "Enums",
		link    : helper.getUniqueFilename( "enums.list" ),
		members : []
	},
	callbacks : {
		title   : "Callbacks",
		link    : helper.getUniqueFilename( "namespaces.list" ),
		members : []
	}
};

function find( spec ) {
	return helper.find( data, spec );
}

function tutoriallink( tutorial ) {
	return helper.toTutorial( tutorial, null, { tag : 'em', classname : 'disabled', prefix : 'Tutorial: ' } );
}

function getAncestorLinks( doclet ) {
	return helper.getAncestorLinks( data, doclet );
}

function hashToLink( doclet, hash ) {
	if ( !/^(#.+)/.test( hash ) ) { return hash; }

	var url = helper.createLink( doclet );

	url = url.replace( /(#.+|$)/, hash );
	return '' + hash + '';
}

function needsSignature( doclet ) {
	var needsSig = false;

	// function and class definitions always get a signature
	if ( doclet.kind === 'function' || doclet.kind === 'class' ) {
		needsSig = true;
	}
	// typedefs that contain functions get a signature, too
	else if ( doclet.kind === 'typedef' && doclet.type && doclet.type.names &&
		doclet.type.names.length ) {
		for ( var i = 0, l = doclet.type.names.length; i < l; i++ ) {
			if ( doclet.type.names[i].toLowerCase() === 'function' ) {
				needsSig = true;
				break;
			}
		}
	}

	return needsSig;
}

function addSignatureParams( f ) {
	var params = helper.getSignatureParams( f, 'optional' );

	f.signature = (f.signature || '') + '(' + params.join( ', ' ) + ')';
}

function addSignatureReturns( f ) {
	var returnTypes = helper.getSignatureReturns( f );

	f.signature = '' + (f.signature || '') + '' + '' + (returnTypes.length ? ' → {' + returnTypes.join( '|' ) + '}' : '') + '';
}

function addSignatureTypes( f ) {
	var types = helper.getSignatureTypes( f );

	f.signature = (f.signature || '') + '' + (types.length ? ' :' + types.join( '|' ) : '') + '';
}

function addAttribs( f ) {
	var attribs = helper.getAttribs( f );

	f.attribs = '' + htmlsafe( attribs.length ? '<' + attribs.join( ', ' ) + '> ' : '' ) + '';
}

function shortenPaths( files, commonPrefix ) {
	// always use forward slashes
	var regexp = new RegExp( '\\\\', 'g' );

	Object.keys( files ).forEach( function ( file ) {
		files[file].shortened = files[file].resolved.replace( commonPrefix, '' )
			.replace( regexp, '/' );
	} );

	return files;
}

function resolveSourcePath( filepath ) {
	return path.resolve( process.cwd(), filepath );
}

function getPathFromDoclet( doclet ) {
	if ( !doclet.meta ) {
		return;
	}

	var filepath = doclet.meta.path && doclet.meta.path !== 'null' ?
		doclet.meta.path + '/' + doclet.meta.filename :
		doclet.meta.filename;

	return filepath;
}

function generate( docType, title, docs, filename, resolveLinks ) {
	resolveLinks = resolveLinks === false ? false : true;

	var docData = {
		title   : title,
		docs    : docs,
		docType : docType
	};

	var outpath = path.join( outdir, filename ),
		html = view.render( 'container.tmpl', docData );

	if ( resolveLinks ) {
		html = helper.resolveLinks( html ); // turn {@link foo} into foo
	}

	fs.writeFileSync( outpath, html, 'utf8' );
}

function generateSourceFiles( sourceFiles ) {
	Object.keys( sourceFiles ).forEach( function ( file ) {
		var source;
		// links are keyed to the shortened path in each doclet's `meta.filename` property
		var sourceOutfile = helper.getUniqueFilename( sourceFiles[file].shortened );
		helper.registerLink( sourceFiles[file].shortened, sourceOutfile );

		try {
			source = {
				kind : 'source',
				code : helper.htmlsafe( fs.readFileSync( sourceFiles[file].resolved, 'utf8' ) )
			};
		}
		catch ( e ) {
			handle( e );
		}

		generate( 'source', 'Source: ' + sourceFiles[file].shortened, [source], sourceOutfile,
			false );
	} );
}

/**
 * Look for classes or functions with the same name as modules (which indicates that the module
 * exports only that class or function), then attach the classes or functions to the `module`
 * property of the appropriate module doclets. The name of each class or function is also updated
 * for display purposes. This function mutates the original arrays.
 *
 * @private
 * @param {Array.<module:jsdoc/doclet.Doclet>} doclets - The array of classes and functions to
 * check.
 * @param {Array.<module:jsdoc/doclet.Doclet>} modules - The array of module doclets to search.
 */
function attachModuleSymbols( doclets, modules ) {
	var symbols = {};

	// build a lookup table
	doclets.forEach( function ( symbol ) {
		symbols[symbol.longname] = symbol;
	} );

	return modules.map( function ( module ) {
		if ( symbols[module.longname] ) {
			module.module = symbols[module.longname];
			module.module.name = module.module.name.replace( 'module:', 'require("' ) + '")';
		}
	} );
}

/*xperiments*/
var sort_by = function(field, reverse, primer){
	var key = function (x) {return primer ? primer(x[field]) : x[field]};

	return function (a,b) {
		var A = key(a), B = key(b);
		return ( (A  B) ? 1 : 0) ) * [-1,1][+!!reverse];
	}
}

/**
 * Create the navigation sidebar.
 * @param {object} members The members that will be used to create the sidebar.
 * @param {array<object>} members.classes
 * @param {array<object>} members.externals
 * @param {array<object>} members.globals
 * @param {array<object>} members.mixins
 * @param {array<object>} members.modules
 * @param {array<object>} members.namespaces
 * @param {array<object>} members.tutorials
 * @param {array<object>} members.events
 * @return {string} The HTML for the navigation sidebar.
 */
function buildNav( members ) {

	var seen = {};
	var nav = navigationMaster;

	if ( members.modules.length ) {

		members.modules.sort(sort_by('longname', true, function(a){return a.toUpperCase()}));
		members.modules.forEach( function ( m ) {
			if ( !hasOwnProp.call( seen, m.longname ) ) {

				nav.module.members.push( linkto( m.longname, m.name ) );
			}
			seen[m.longname] = true;
		} );
	}

	if ( members.externals.length ) {

		members.externals.sort(sort_by('longname', true, function(a){return a.toUpperCase()}));
		members.externals.forEach( function ( e ) {
			if ( !hasOwnProp.call( seen, e.longname ) ) {

				nav.external.members.push( linkto( e.longname, e.name.replace( /(^"|"$)/g, '' ) ) );
			}
			seen[e.longname] = true;
		} );
	}

	if ( members.classes.length ) {

		members.classes.sort(sort_by('longname', true, function(a){return a.toUpperCase()}));
		members.classes.forEach( function ( c ) {
			if ( !hasOwnProp.call( seen, c.longname ) ) {

				nav.class.members.push( { link: linkto( c.longname, c.longname ), namespace:c} );
			}
			seen[c.longname] = true;
		} );

	}
	/*
	 if ( members.events.length ) {

	 members.events.forEach( function ( e ) {
	 if ( !hasOwnProp.call( seen, e.longname ) ) {

	 nav.event.members.push( linkto( e.longname, e.name ) );

	 }
	 seen[e.longname] = true;
	 } );

	 }*/

	if ( members.typedef.length ) {

		members.typedef.forEach( function ( td ) {
			if ( !hasOwnProp.call( seen, td.longname ) ) {

				nav.typedef.members.push( {link:linkto( td.name, td.name ), namespace:{ longname:td.longname } });

			}
			seen[td.longname] = true;
		} );

	}
	if ( members.callbacks.length ) {

		members.callbacks.forEach( function ( cb ) {
			if ( !hasOwnProp.call( seen, cb.longname ) ) {

				nav.callbacks.members.push( {link:linkto( cb.longname, cb.longname.split('#')[0] ), namespace:{ longname:cb.longname } });

			}
			seen[cb.longname] = true;
		} );

	}
	if ( members.namespaces.length ) {

		members.namespaces.sort(sort_by('longname', true, function(a){return a.toUpperCase()}));
		members.namespaces.forEach( function ( n ) {

			if ( !hasOwnProp.call( seen, n.longname ))
			{
				nav.namespace.members.push( { link: linkto( n.longname, n.longname ), namespace:n} );
			}
			seen[n.longname] = true;
		} );

	}

	if ( members.mixins.length ) {

		members.mixins.sort(sort_by('longname', true, function(a){return a.toUpperCase()}));
		members.mixins.forEach( function ( m ) {
			if ( !hasOwnProp.call( seen, m.longname ) ) {

				nav.mixin.members.push( linkto( m.longname, m.longname ) );
			}
			seen[m.longname] = true;
		} );

	}

	if ( members.tutorials.length ) {

		members.tutorials.sort(sort_by('name', true, function(a){return a.toUpperCase()}));
		members.tutorials.forEach( function ( t ) {

			nav.tutorial.members.push( tutoriallink( t.name ) );
		} );

	}

	if ( members.globals.length ) {
		members.globals.sort(sort_by('longname', true, function(a){return a.toUpperCase()}));
		members.globals.forEach( function ( g ) {
			if ( g.kind !== 'typedef' && !hasOwnProp.call( seen, g.longname ) ) {

				nav.global.members.push( linkto( g.longname, g.longname ) );
			}
			seen[g.longname] = true;
		} );
	}

	var topLevelNav = [];
	_.each( nav, function ( entry, name ) {
		if ( entry.members.length > 0 && name !== "index" ) {
			topLevelNav.push( {
				title   : entry.title,
				link    : entry.link,
				members : entry.members
			} );
		}
	} );

	nav.topLevelNav = topLevelNav;

}

/**
 @param {TAFFY} taffyData See <http://taffydb.com/>.
 @param {object} opts
 @param {Tutorial} tutorials
 */
exports.publish = function ( taffyData, opts, tutorials ) {
	data = taffyData;

	conf['default'] = conf['default'] || {};

	var templatePath = opts.template;
	view = new template.Template( templatePath + '/tmpl' );

	// claim some special filenames in advance, so the All-Powerful Overseer of Filename Uniqueness
	// doesn't try to hand them out later
//	var indexUrl = helper.getUniqueFilename( 'index' );
	// don't call registerLink() on this one! 'index' is also a valid longname

//	var globalUrl = helper.getUniqueFilename( 'global' );
	helper.registerLink( 'global', globalUrl );

	// set up templating
	view.layout = 'layout.tmpl';

	// set up tutorials for helper
	helper.setTutorials( tutorials );

	data = helper.prune( data );
	data.sort( 'longname, version, since' );
	helper.addEventListeners( data );

	var sourceFiles = {};
	var sourceFilePaths = [];
	data().each( function ( doclet ) {
		doclet.attribs = '';

		if ( doclet.examples ) {
			doclet.examples = doclet.examples.map( function ( example ) {
				var caption, code;

				if ( example.match( /^\s*
([\s\S]+?)<\/caption>(\s*[\n\r])([\s\S]+)$/i ) ) {
					caption = RegExp.$1;
					code = RegExp.$3;
				}

				return {
					caption : caption || '',
					code    : code || example
				};
			} );
		}
		if ( doclet.see ) {
			doclet.see.forEach( function ( seeItem, i ) {
				doclet.see[i] = hashToLink( doclet, seeItem );
			} );
		}

		// build a list of source files
		var sourcePath;
		var resolvedSourcePath;
		if ( doclet.meta ) {
			sourcePath = getPathFromDoclet( doclet );
			resolvedSourcePath = resolveSourcePath( sourcePath );
			sourceFiles[sourcePath] = {
				resolved  : resolvedSourcePath,
				shortened : null
			};

			sourceFilePaths.push( resolvedSourcePath );
		}
	} );

	// update outdir if necessary, then create outdir
	var packageInfo = ( find( {kind : 'package'} ) || [] ) [0];
	if ( packageInfo && packageInfo.name ) {
		outdir = path.join( outdir, packageInfo.name, packageInfo.version );
	}
	fs.mkPath( outdir );

	// copy static files to outdir
	var fromDir = path.join( templatePath, 'static' ),
		staticFiles = fs.ls( fromDir, 3 );

	staticFiles.forEach( function ( fileName ) {
		var toDir = fs.toDir( fileName.replace( fromDir, outdir ) );
		fs.mkPath( toDir );
		fs.copyFileSync( fileName, toDir );
	} );

	if ( sourceFilePaths.length ) {
		sourceFiles = shortenPaths( sourceFiles, path.commonPrefix( sourceFilePaths ) );
	}
	data().each( function ( doclet ) {

		var url = helper.createLink( doclet );
		helper.registerLink( doclet.longname, url );

		// replace the filename with a shortened version of the full path
		var docletPath;
		if ( doclet.meta ) {
			docletPath = getPathFromDoclet( doclet );
			docletPath = sourceFiles[docletPath].shortened;
			if ( docletPath ) {
				doclet.meta.filename = docletPath;
			}
		}
	} );

	data().each( function ( doclet ) {
		var url = helper.longnameToUrl[doclet.longname];

		if ( url.indexOf( '#' ) > -1 ) {
			doclet.id = helper.longnameToUrl[doclet.longname].split( /#/ ).pop();
		}
		else {
			doclet.id = doclet.name;
		}

		if ( needsSignature( doclet ) ) {
			addSignatureParams( doclet );
			addSignatureReturns( doclet );
			addAttribs( doclet );
		}
	} );

	// do this after the urls have all been generated
	data().each( function ( doclet ) {
		doclet.ancestors = getAncestorLinks( doclet );

		if ( doclet.kind === 'member' ) {
			addSignatureTypes( doclet );
			addAttribs( doclet );
		}

		if ( doclet.kind === 'constant' ) {
			addSignatureTypes( doclet );
			addAttribs( doclet );
			doclet.kind = 'member';
		}
	} );

	var members = getMembers2( data );
	members.tutorials = tutorials.children;

	// add template helpers
	view.find = find;
	view.linkto = linkto;
	view.resolveAuthorLinks = resolveAuthorLinks;
	view.tutoriallink = tutoriallink;
	view.htmlsafe = htmlsafe;

	// once for all
	buildNav( members );
	view.nav = navigationMaster;
	view.navOptions = navOptions;
	attachModuleSymbols( find( { kind : ['class', 'function'], longname : {left : 'module:'} } ),
		members.modules );

	// only output pretty-printed source files if requested; do this before generating any other
	// pages, so the other pages can link to the source files
	if ( conf.outputSourceFiles ) {
		generateSourceFiles( sourceFiles );
	}

	if ( members.globals.length ) {
		generate( 'global', 'Global', [
			{kind : 'globalobj'}
		], globalUrl );
	}

	// some browsers can't make the dropdown work
	if ( view.nav.module && view.nav.module.members.length ) {
		generate( 'module', view.nav.module.title, [
			{kind : 'sectionIndex', contents : view.nav.module}
		], navigationMaster.module.link );
	}

	if ( view.nav.class && view.nav.class.members.length ) {
		generate( 'class', view.nav.class.title, [
			{kind : 'sectionIndex', contents : view.nav.class}
		], navigationMaster.class.link );
	}

	if ( view.nav.namespace && view.nav.namespace.members.length ) {
		generate( 'namespace', view.nav.namespace.title, [
			{kind : 'sectionIndex', contents : view.nav.namespace}
		], navigationMaster.namespace.link );
	}

	if ( view.nav.mixin && view.nav.mixin.members.length ) {
		generate( 'mixin', view.nav.mixin.title, [
			{kind : 'sectionIndex', contents : view.nav.mixin}
		], navigationMaster.mixin.link );
	}

	if ( view.nav.external && view.nav.external.members.length ) {
		generate( 'external', view.nav.external.title, [
			{kind : 'sectionIndex', contents : view.nav.external}
		], navigationMaster.external.link );
	}

	if ( view.nav.tutorial && view.nav.tutorial.members.length ) {
		generate( 'tutorial', view.nav.tutorial.title, [
			{kind : 'sectionIndex', contents : view.nav.tutorial}
		], navigationMaster.tutorial.link );
	}

	if ( view.nav.typedef && view.nav.typedef.members.length ) {
		generate( 'typedef', view.nav.typedef.title, [
			{kind : 'sectionIndex', contents : view.nav.typedef}
		], navigationMaster.typedef.link );
	}

	if ( view.nav.callbacks && view.nav.callbacks.members.length ) {
		generate( 'callbacks', view.nav.callbacks.title, [
			{kind : 'sectionIndex', contents : view.nav.callbacks}
		], navigationMaster.callbacks.link );
	}

	// index page displays information from package.json and lists files
	var files = find( {kind : 'file'} ),
		packages = find( {kind : 'package'} );

	generate( 'index', 'Index',
		packages.concat(
			[
				{kind : 'mainpage', readme : opts.readme, longname : (opts.mainpagetitle) ? opts.mainpagetitle : 'Main Page'}
			]
		).concat( files ),
		indexUrl );

	// set up the lists that we'll use to generate pages
	var classes = taffy( members.classes );
	var modules = taffy( members.modules );
	var namespaces = taffy( members.namespaces );
	var mixins = taffy( members.mixins );
	var externals = taffy( members.externals );
	var typedefs = taffy( members.typedef );
	var callbacks = taffy( members.callbacks );

	for ( var longname in helper.longnameToUrl ) {
		if ( hasOwnProp.call( helper.longnameToUrl, longname ) ) {
			var myClasses = helper.find( classes, {longname : longname} );
			if ( myClasses.length )
			{
				var dta = myClasses[0];
				if( dta && dta.tags )
				{
					var isInterface = false;
					for( var i=0; i<dta.tags.length; i++ )
					{
						if( dta.tags[i].title=="interface")
						{
							isInterface = true;
						}
					}

					if( isInterface )
					{
						generate( 'class', 'Interface: ' + myClasses[0].name, myClasses, helper.longnameToUrl[longname] );
					}
					else
					{
						generate( 'class', 'Class: ' + myClasses[0].name, myClasses, helper.longnameToUrl[longname] );
					}
				}
				else
				{
					generate( 'class', 'Class: ' + myClasses[0].name, myClasses, helper.longnameToUrl[longname] );
				}
			}

			var myModules = helper.find( modules, {longname : longname} );
			if ( myModules.length ) {
				generate( 'module', 'Module: ' + myModules[0].name, myModules, helper.longnameToUrl[longname] );
			}
			/*xperiments*/
			var myNamespaces = helper.find( namespaces, {longname : longname} );
			if ( myNamespaces.length ) {
				//group repeated namespaces
				var outNS = [];
				var namesNS = [];
				for( var ns = 0, total = myNamespaces.length; ns<total; ns++ )
				{
					if( namesNS.indexOf( myNamespaces[ns].longname)==-1 )
					{
						outNS.push( myNamespaces[ns]);
						namesNS.push( myNamespaces[ns].longname );
					}
				}
				generate( 'namespace', 'Namespace: ' + outNS[0].longname, outNS, helper.longnameToUrl[longname] );
			}

			var myMixins = helper.find( mixins, {longname : longname} );
			if ( myMixins.length ) {
				generate( 'mixin', 'Mixin: ' + myMixins[0].name, myMixins, helper.longnameToUrl[longname] );
			}

			var myExternals = helper.find( externals, {longname : longname} );
			if ( myExternals.length ) {
				generate( 'external', 'External: ' + myExternals[0].name, myExternals, helper.longnameToUrl[longname] );
			}
			var myTypedefs = helper.find( typedefs, {longname : longname} );
			if ( myTypedefs.length ) {
				generate( 'typedef', 'Enums: ' + myTypedefs[0].name, myTypedefs, helper.longnameToUrl[longname] );
			}
			var myCallbacks = helper.find( callbacks, {longname : longname} );
			if ( myCallbacks.length ) {
				if( longname.indexOf('#')==-1)
				{
					generate( 'callbacks', 'Callbacks: ' + myCallbacks[0].name, myCallbacks, helper.longnameToUrl[longname] );
				}
				else
				{
					generate( 'callbacks', 'Callbacks: ' + myCallbacks[0].name, myCallbacks, longname.split('#')[0] );
				}
			}
		}
	}

	// TODO: move the tutorial functions to templateHelper.js
	function generateTutorial( title, tutorial, filename ) {
		var tutorialData = {
			title    : title,
			header   : tutorial.title,
			content  : tutorial.parse(),
			children : tutorial.children,
			docs     : null
		};

		var tutorialPath = path.join( outdir, filename ),
			html = view.render( 'tutorial.tmpl', tutorialData );

		// yes, you can use {@link} in tutorials too!
		html = helper.resolveLinks( html ); // turn {@link foo} into foo

		fs.writeFileSync( tutorialPath, html, 'utf8' );
	}

	// tutorials can have only one parent so there is no risk for loops
	function saveChildren( node ) {
		node.children.forEach( function ( child ) {
			generateTutorial( 'tutorial' + child.title, child, helper.tutorialToUrl( child.name ) );
			saveChildren( child );
		} );
	}

	if( tutorials && tutorials.length>0) saveChildren( tutorials );
};


'use strict';

var REGEX_PREFIX = "!regexp:/",
    sh = require('shelljs'),
    glob = require('glob'),
    fs = require('fs'),
    Path = require('path'),
    config = require('./config'),
    inch_config = require('../../package.json');

// Removes absolute paths from JSON output and inject
// some relevant metadata, e.g. language.
var run = function(objects, inch_args) {
  var cwd = process.cwd();
  var excluded = config.files().excluded || [];
  var data = {
    language: 'javascript',
    client_name: 'inchjs',
    args: inch_args,
    client_version: ""+inch_config.version,
    git_repo_url: getGitRepoURL()
  };
  if( process.env.TRAVIS ) {
    data['travis'] = true;
    data['travis_job_id'] = process.env.TRAVIS_JOB_ID;
    data['travis_commit'] = process.env.TRAVIS_COMMIT;
    data['travis_repo_slug'] = process.env.TRAVIS_REPO_SLUG;
  }
  data['branch_name'] = getGitBranchName();
  data['objects'] = objects.filter(includeObjectFilter).map(function(item) {
        return prepareCodeObject(item, cwd);
      }).filter(function(item) {
        return !excludeObjectIfMatch(item, excluded);
      });
  return data;
}

var excludeObjectFilter = function(item) {
  var name = item.longname;
  var is_undefined = name.indexOf('[undefined]') > -1;
  var is_invalid_kind = item.kind == 'typedef' ||
                          item.kind == 'member' ||
                          item.kind == 'package';
  var is_part_of_module = name.indexOf('module:') == 0 &&
                            name.indexOf('~') > -1 &&
                            name.indexOf('~') == name.lastIndexOf('~');
  return  is_undefined ||
          is_invalid_kind ||
          (!is_part_of_module && name.indexOf('~') > -1);
}

var excludeObjectIfMatch = function(item, excluded) {
  var meta = item.meta;
  if( meta === undefined ) return false;
  var filename = Path.join(meta.path, meta.filename);
  for(var i=0;i<excluded.length;i++) {
    if( matchesFilename(excluded[i], filename) ) return true;
  }
  return false;
}

function matchesFilename(pattern, _filename) {
  var filename = _filename.replace(/^(\/)/, '')
  if( pattern.indexOf(REGEX_PREFIX) == 0 ) {
    var str = pattern.substr(REGEX_PREFIX.length, pattern.length-REGEX_PREFIX.length-1);
    var regex = new RegExp(str, "i");
    return filename.match(regex) !== null;
  } else if( pattern.indexOf('*') != -1 ) {
    var results = glob.sync(pattern);
    for(var i=0;i<results.length;i++) {
      if( results[i] == filename ) return true;
    }
    return false;
  } else {
    return filename.indexOf(pattern) == 0;
  }
}

var includeObjectFilter = function(item) {
  return !excludeObjectFilter(item);
}

var getGitBranchName = function() {
  return getTrimmedOutput("git rev-parse --abbrev-ref HEAD");
}

var getGitRepoURL = function() {
  return getTrimmedOutput("git config --get remote.origin.url");
}

var getTrimmedOutput = function(cmd) {
  var out = sh.exec(cmd, {silent: true}).output
  return out.trim();
}

var prepareCodeObject = function(item, cwd) {
  if( item['meta'] && typeof(item['meta']) == "object" ) {
    if( item['comment'] == '' ) {
      var filename = Path.join(item['meta']['path'], item['meta']['filename']);
      var line = item['meta']['lineno'];
      item['comment'] = readCommentsFromFilePrecedingLine(filename, line);
    }
    item['comment'] = trimComment(item['comment']);
    item['meta']['path'] = item['meta']['path'].replace(cwd, ''); // .replace(/^(\/)/, '')
  }
  return item;
}

var trimComment = function(comment) {
  return (comment || "").split("\n").map(function(line) {
    return line.trim();
  }).join("\n");
}

var readCommentsFromFilePrecedingLine = function(filename, lineno) {
  var contents = fs.readFileSync(filename, 'utf-8'),
      lines = contents.split('\n'),
      result = [];
  for(var i=lineno-2;i>=0;i--) {
    var line = lines[i];
    if( line.match(/^\s*[\/\*]/) ) {
      result.unshift(line);
    } else {
      return result.join('\n');
    }
  }
  return result.join('\n');
}

module.exports = {
  run: run
};


var http        = require('http');
var tls         = require('tls');
var inherits    = require('util').inherits;
var socksClient = require('socks5-client');
var Promise     = require('bluebird');
var openports   = require('openports');
var tmp         = Promise.promisifyAll(require('tmp'));
var Tor         = require('./tor');

/**
 * An HTTP Agent for proxying requests through Tor using SOCKS5.
 *
 * @param {object} opts
 */
function TorAgent(opts) {
  if (!(this instanceof TorAgent)) {
    return new TorAgent();
  }

  http.Agent.call(this, opts);

  this.socksHost   = opts.socksHost || 'localhost';
  this.socksPort   = opts.socksPort || 9050;
  this.defaultPort = 80;

  // Used when invoking TorAgent.create
  this.tor = opts.tor;

  // Prevent protocol check, wrap destroy
  this.protocol = null;
  this.defaultDestroy = this.destroy;
  this.destroy = this.destroyWrapper;
}

inherits(TorAgent, http.Agent);

/**
 * Spawns a Tor process listening on a random unused port, and creates an
 * agent for use with HTTP(S) requests. The optional verbose param will enable
 * console output while initializing Tor. Accepts an optional callback,
 * otherwise it returns a promise that resolves with an instance of TorAgent.
 * Note that since the Tor process is using a new DataDirectory with no cached
 * microdescriptors or any other state, bootstrapping can take between 15 - 60s.
 * The resulting child process is automatically killed when node exits.
 *
 * @param   {boolean}  [verbose] Whether or not to enable console output
 * @param   {function} [fn]      Optional callback to invoke
 * @returns {Promise}  A promise that resoles with the TorAgent
 */
TorAgent.create = function(verbose, fn) {
  var port;

  return openports(1).then(function(ports) {
    port = ports[0];
    return tmp.dirAsync({template: '/tmp/toragent-XXXXXX'});
  }).then(function(res) {
    if (verbose) {
      console.log('Spawning Tor');
    }
    return Tor.spawn(port, res[0], 30000);
  }).then(function(tor) {
    if (verbose) {
      console.log('Tor spawned with pid', tor.process.pid,
        'listening on', tor.port);
    }

    return new TorAgent({
      socksHost: 'localhost',
      socksPort: port,
      tor:       tor
    });
  }).nodeify(fn);
};

/**
 * Rotates the IP address used by Tor by sending a SIGHUP. Returns a promise
 * that resolves when complete.
 *
 * @returns {Promise}
 */
TorAgent.prototype.rotateAddress = function() {
  return this.tor.rotateAddress();
};

/**
 * Closes all sockets handled by the agent, and closes the Tor process. Returns
 * a promise that resolves when the Tor process has closed.
 *
 * @returns {Promise}
 */
TorAgent.prototype.destroyWrapper = function() {
  this.defaultDestroy();
  return this.tor.destroy();
};

/**
 * Creates a TCP connection through the specified SOCKS5 server. Updates the
 * request options object to handle both HTTP and HTTPs.
 *
 * @param   {object}             opts Options object, as passed by addRequest
 * @returns {Socks5ClientSocket} The SOCKS5 connection
 */
TorAgent.prototype.createConnection = function(opts) {
  // Change protocol if https
  var protocol = opts.protocol || opts.uri.protocol;
  if (protocol === 'https:') {
    opts.port = (opts.port === 80) ? 443 : opts.port;
  } else {
  // If not https, use socksClient default
    return socksClient.createConnection(opts);
  }

  var socksSocket = socksClient.createConnection(opts);
  var connectHandler = socksSocket.handleSocksConnectToHost;

  socksSocket.handleSocksConnectToHost = function() {
    opts.socket = socksSocket.socket;
    opts.servername = opts.hostname;

    socksSocket.socket = tls.connect(opts, function() {
      socksSocket.authorized = socksSocket.socket.authorized;
      connectHandler.call(socksSocket);
    }).on('error', function(err) {
      socksSocket.emit('error', err);
    });
  };

  return socksSocket;
}

module.exports = TorAgent;


//  ----------------------------------------------------------------------------
//
//  bootstrap-typeahead.js
//
//  Twitter Bootstrap Typeahead Plugin
//  v1.2.2
//  https://github.com/tcrosen/twitter-bootstrap-typeahead
//
//
//  Author
//  ----------
//  Terry Rosen
//  tcrosen@gmail.com | @rerrify | github.com/tcrosen/
//
//
//  Description
//  ----------
//  Custom implementation of Twitter's Bootstrap Typeahead Plugin
//  http://twitter.github.com/bootstrap/javascript.html#typeahead
//
//
//  Requirements
//  ----------
//  jQuery 1.7+
//  Twitter Bootstrap 2.0+
//
//  ----------------------------------------------------------------------------

!
function ($) {

    "use strict";

    //------------------------------------------------------------------
    //
    //  Constructor
    //
    var Typeahead = function (element, options) {
        this.$element = $(element);
        this.options = $.extend(true, {}, $.fn.typeahead.defaults, options);
        this.$menu = $(this.options.menu).appendTo('body');
        this.shown = false;

        // Method overrides
        this.eventSupported = this.options.eventSupported || this.eventSupported;
        this.grepper = this.options.grepper || this.grepper;
        this.highlighter = this.options.highlighter || this.highlighter;
        this.lookup = this.options.lookup || this.lookup;
        this.matcher = this.options.matcher || this.matcher;
        this.render = this.options.render || this.render;
        this.select = this.options.select || this.select;
        this.sorter = this.options.sorter || this.sorter;
        this.source = this.options.source || this.source;

        if (!this.source.length) {
            var ajax = this.options.ajax;

            if (typeof ajax === 'string') {
                this.ajax = $.extend({}, $.fn.typeahead.defaults.ajax, { url: ajax });
            } else {
                this.ajax = $.extend({}, $.fn.typeahead.defaults.ajax, ajax);
            }

            if (!this.ajax.url) {
                this.ajax = null;
            }
        }

        this.listen();
    }

    Typeahead.prototype = {

        constructor: Typeahead,

        //=============================================================================================================
        //
        //  Utils
        //
        //=============================================================================================================

        //------------------------------------------------------------------
        //
        //  Check if an event is supported by the browser eg. 'keypress'
        //  * This was included to handle the "exhaustive deprecation" of jQuery.browser in jQuery 1.8
        //
        eventSupported: function(eventName) {
            var isSupported = (eventName in this.$element);

            if (!isSupported) {
              this.$element.setAttribute(eventName, 'return;');
              isSupported = typeof this.$element[eventName] === 'function';
            }

            return isSupported;
        },

        //=============================================================================================================
        //
        //  AJAX
        //
        //=============================================================================================================

        //------------------------------------------------------------------
        //
        //  Handle AJAX source
        //
        ajaxer: function () {
            var that = this,
                query = that.$element.val();

            if (query === that.query) {
                return that;
            }

            // Query changed
            that.query = query;

            // Cancel last timer if set
            if (that.ajax.timerId) {
                clearTimeout(that.ajax.timerId);
                that.ajax.timerId = null;
            }

            if (!query || query.length < that.ajax.triggerLength) {
                // Cancel the ajax callback if in progress
                if (that.ajax.xhr) {
                    that.ajax.xhr.abort();
                    that.ajax.xhr = null;
                    that.ajaxToggleLoadClass(false);
                }

                return that.shown ? that.hide() : that;
            }

            // Query is good to send, set a timer
            that.ajax.timerId = setTimeout(function() {
                $.proxy(that.ajaxExecute(query), that)
            }, that.ajax.timeout);

            return that;
        },

        //------------------------------------------------------------------
        //
        //  Execute an AJAX request
        //
        ajaxExecute: function(query) {
            this.ajaxToggleLoadClass(true);

            // Cancel last call if already in progress
            if (this.ajax.xhr) this.ajax.xhr.abort();

            var params = this.ajax.preDispatch ? this.ajax.preDispatch(query) : { query : query };
            var jAjax = (this.ajax.method === "post") ? $.post : $.get;
            this.ajax.xhr = jAjax(this.ajax.url, params, $.proxy(this.ajaxLookup, this));
            this.ajax.timerId = null;
        },

        //------------------------------------------------------------------
        //
        //  Perform a lookup in the AJAX results
        //
        ajaxLookup: function (data) {
            var items;

            this.ajaxToggleLoadClass(false);

            if (!this.ajax.xhr) return;

            if (this.ajax.preProcess) {
                data = this.ajax.preProcess(data);
            }

            // Save for selection retreival
            this.ajax.data = data;

            items = this.grepper(this.ajax.data);

            if (!items || !items.length) {
                return this.shown ? this.hide() : this;
            }

            this.ajax.xhr = null;

            return this.render(items.slice(0, this.options.items)).show();
        },

        //------------------------------------------------------------------
        //
        //  Toggle the loading class
        //
        ajaxToggleLoadClass: function (enable) {
            if (!this.ajax.loadingClass) return;
            this.$element.toggleClass(this.ajax.loadingClass, enable);
        },

        //=============================================================================================================
        //
        //  Data manipulation
        //
        //=============================================================================================================

        //------------------------------------------------------------------
        //
        //  Search source
        //
        lookup: function (event) {
            var that = this,
                items;

            if (that.ajax) {
                that.ajaxer();
            }
            else {
                that.query = that.$element.val();

                if (!that.query) {
                    return that.shown ? that.hide() : that;
                }

                items = that.grepper(that.source);

                if (!items || !items.length) {
                    return that.shown ? that.hide() : that;
                }

                return that.render(items.slice(0, that.options.items)).show();
            }
        },

        //------------------------------------------------------------------
        //
        //  Filters relevent results
        //
        grepper: function(data) {
            var that = this,
                items;

            if (data && data.length && !data[0].hasOwnProperty(that.options.display)) {
                return null;
            }

            items = $.grep(data, function (item) {
                return that.matcher(item[that.options.display], item);
            });

            return this.sorter(items);
        },

        //------------------------------------------------------------------
        //
        //  Looks for a match in the source
        //
        matcher: function (item) {
            return ~item.toLowerCase().indexOf(this.query.toLowerCase());
        },

        //------------------------------------------------------------------
        //
        //  Sorts the results
        //
        sorter: function (items) {
            var that = this,
                beginswith = [],
                caseSensitive = [],
                caseInsensitive = [],
                item;

            while (item = items.shift()) {
                if (!item[that.options.display].toLowerCase().indexOf(this.query.toLowerCase())) {
                    beginswith.push(item);
                }
                else if (~item[that.options.display].indexOf(this.query)) {
                    caseSensitive.push(item);
                }
                else {
                    caseInsensitive.push(item);
                }
            }

            return beginswith.concat(caseSensitive, caseInsensitive);
        },

        //=============================================================================================================
        //
        //  DOM manipulation
        //
        //=============================================================================================================

        //------------------------------------------------------------------
        //
        //  Shows the results list
        //
        show: function () {
            var pos = $.extend({}, this.$element.offset(), {
                height: this.$element[0].offsetHeight
            });

            this.$menu.css({
                top: pos.top + pos.height,
                left: pos.left
            });

            this.$menu.show();
            this.shown = true;

            return this;
        },

        //------------------------------------------------------------------
        //
        //  Hides the results list
        //
        hide: function () {
            this.$menu.hide();
            this.shown = false;
            return this;
        },

        //------------------------------------------------------------------
        //
        //  Highlights the match(es) within the results
        //
        highlighter: function (item) {
            var query = this.query.replace(/[\-\[\]{}()*+?.,\\\^$|#\s]/g, '\\$&');
            return item.replace(new RegExp('(' + query + ')', 'ig'), function ($1, match) {
                return '' + match + '';
            });
        },

        //------------------------------------------------------------------
        //
        //  Renders the results list
        //
        render: function (items) {
            var that = this;

            items = $(items).map(function (i, item) {
                i = $(that.options.item).attr('data-value', item[that.options.val]);
                i.find('a').html(that.highlighter(item[that.options.display], item));
                return i[0];
            });

            items.first().addClass('active');
            this.$menu.html(items);
            return this;
        },

        //------------------------------------------------------------------
        //
        //  Item is selected
        //
        select: function () {
            var $selectedItem = this.$menu.find('.active');
            this.$element.val($selectedItem.text()).change();
            this.options.itemSelected($selectedItem, $selectedItem.attr('data-value'), $selectedItem.text());
            return this.hide();
        },

        //------------------------------------------------------------------
        //
        //  Selects the next result
        //
        next: function (event) {
            var active = this.$menu.find('.active').removeClass('active');
            var next = active.next();

            if (!next.length) {
                next = $(this.$menu.find('li')[0]);
            }

            next.addClass('active');
        },

        //------------------------------------------------------------------
        //
        //  Selects the previous result
        //
        prev: function (event) {
            var active = this.$menu.find('.active').removeClass('active');
            var prev = active.prev();

            if (!prev.length) {
                prev = this.$menu.find('li').last();
            }

            prev.addClass('active');
        },

        //=============================================================================================================
        //
        //  Events
        //
        //=============================================================================================================

        //------------------------------------------------------------------
        //
        //  Listens for user events
        //
        listen: function () {
            this.$element.on('blur', $.proxy(this.blur, this))
                         .on('keyup', $.proxy(this.keyup, this));

    		if (this.eventSupported('keydown')) {
				this.$element.on('keydown', $.proxy(this.keypress, this));
			} else {
				this.$element.on('keypress', $.proxy(this.keypress, this));
			}

            this.$menu.on('click', $.proxy(this.click, this))
                      .on('mouseenter', 'li', $.proxy(this.mouseenter, this));
        },

        //------------------------------------------------------------------
        //
        //  Handles a key being raised up
        //
        keyup: function (e) {
            e.stopPropagation();
            e.preventDefault();

            switch (e.keyCode) {
                case 40:
                    // down arrow
                case 38:
                    // up arrow
                    break;
                case 9:
                    // tab
                case 13:
                    // enter
                    if (!this.shown) {
                        return;
                    }
                    this.select();
                    break;
                case 27:
                    // escape
                    this.hide();
                    break;
                default:
                    this.lookup();
            }
        },

        //------------------------------------------------------------------
        //
        //  Handles a key being pressed
        //
        keypress: function (e) {
            e.stopPropagation();
            if (!this.shown) {
                return;
            }

            switch (e.keyCode) {
                case 9:
                    // tab
                case 13:
                    // enter
                case 27:
                    // escape
                    e.preventDefault();
                    break;
                case 38:
                    // up arrow
                    e.preventDefault();
                    this.prev();
                    break;
                case 40:
                    // down arrow
                    e.preventDefault();
                    this.next();
                    break;
            }
        },

        //------------------------------------------------------------------
        //
        //  Handles cursor exiting the textbox
        //
        blur: function (e) {
            var that = this;
            e.stopPropagation();
            e.preventDefault();
            setTimeout(function () {
                if (!that.$menu.is(':focus')) {
                  that.hide();
                }
            }, 150)
        },

        //------------------------------------------------------------------
        //
        //  Handles clicking on the results list
        //
        click: function (e) {
            e.stopPropagation();
            e.preventDefault();
            this.select();
        },

        //------------------------------------------------------------------
        //
        //  Handles the mouse entering the results list
        //
        mouseenter: function (e) {
            this.$menu.find('.active').removeClass('active');
            $(e.currentTarget).addClass('active');
        }
    }

    //------------------------------------------------------------------
    //
    //  Plugin definition
    //
    $.fn.typeahead = function (option) {
        return this.each(function () {
            var $this = $(this),
                data = $this.data('typeahead'),
                options = typeof option === 'object' && option;

            if (!data) {
                $this.data('typeahead', (data = new Typeahead(this, options)));
            }

            if (typeof option === 'string') {
                data[option]();
            }
        });
    }

    //------------------------------------------------------------------
    //
    //  Defaults
    //
    $.fn.typeahead.defaults = {
        source: [],
        items: 8,
        menu: '

',
        item: '

',
        display: 'name',
        val: 'id',
        itemSelected: function () { },
        ajax: {
            url: null,
            timeout: 300,
            method: 'post',
            triggerLength: 3,
            loadingClass: null,
            displayField: null,
            preDispatch: null,
            preProcess: null
        }
    }

    $.fn.typeahead.Constructor = Typeahead;

    //------------------------------------------------------------------
    //
    //  DOM-ready call for the Data API (no-JS implementation)
    //
    //  Note: As of Bootstrap v2.0 this feature may be disabled using $('body').off('.data-api')
    //  More info here: https://github.com/twitter/bootstrap/tree/master/js
    //
    $(function () {
        $('body').on('focus.typeahead.data-api', '[data-provide="typeahead"]', function (e) {
            var $this = $(this);

            if ($this.data('typeahead')) {
                return;
            }

            e.preventDefault();
            $this.typeahead($this.data());
        })
    });

} (window.jQuery);


'use strict';

var retriever = require('./docs/jsdoc_runner'),
    inch_config = require('../package.json'),
    PathExtractor = require("../lib/path_extractor.js"),
    LocalInch = require('./reporter/local'),
    RemoteInch = require('./reporter/remote');

// Vaporware/Internal:
// Generates a report for Inch CI.
function report() {
  retriever.run(null, function(filename) {
    RemoteInch.run(filename, noop);
  });
}

// Runs a local version of Inch.
function run(inch_args, options) {
  var callback = function(filename) {
      LocalInch.run(inch_args || ['suggest'], filename, noop);
    }

  if( options.dry_run ) callback = noop;
  retriever.run(PathExtractor.extractPaths(inch_args), callback);
}

function runSuggest(done, inch_args) {
  var filename = retriever.run();
  LocalInch.run(inch_args || ['suggest'], filename, noop);
}

function runList(done, inch_args) {
  var filename = retriever.run();
  LocalInch.run(inch_args || ['list'], filename, noop);
}

// @private
function noop(stdout, stderr) {
}

// Puts the given string +stdout+ to stdout.
function puts(stdout, stderr) {
  console.log(stdout);
}

module.exports = {
  report: report,
  run: run,
  suggest: runSuggest,
  list: runList,

  version: inch_config.version
}


(function(root) {
  'use strict';

  function unique(col) {
    var u = {}, a = [];
    for(var i = 0, l = col.length; i < l; ++i){
      if(u.hasOwnProperty(col[i])) {
        continue;
      }
      a.push(col[i]);
      u[col[i]] = 1;
    }
    return a;
  }

  function find(col, pred) {
    var value;
    col.forEach(function(item) {
      var result = pred(item);
      if (result) {
        value = item;
      }
    });
    return value;
  }

  function max(array, fn) {
    var max = -Infinity;
    var index;
    for (var i = 0; i < array.length; i++) {
      var result = fn(array[i]);
      if (result >= max) {
        max = result;
        index = i;
      }
    }
    return typeof index !== 'undefined' ? array[index] : max;
  }

  function sortBy(col, fn) {
   col = [].slice.call(col);
   return col.sort(fn);
  }

  function C45() {
    if (!(this instanceof C45)) {
      return new C45();
    }
    this.features = [];
    this.targets = [];
    this.model = {}
    this.target = ''
  }

  C45.prototype = {
    /**
     * train
     *
     * @param {object} options
     * @param {array} options.data - training data
     * @param {string} options.target - class label
     * @param {array} options.features - features names
     * @param {array} options.featureTypes - features type (ie 'category', 'number')
     * @param {function} callback - callback, containing error and model as parameters
     */
    train: function(options, callback) {
      var data = options.data;
      var target = options.target;
      var features = options.features;
      var featureTypes = options.featureTypes;

      featureTypes.forEach(function(f) {
        if (['number','category'].indexOf(f) === -1) {
          callback(new Error('Unrecognized feature type'));
          return;
        }
      });

      var targets = unique(data.map(function(d) {
        return d[d.length-1];
      }));
      this.features = features;
      this.targets = targets;
      this.target = target

      var classify = this.classify.bind(this)

      var model = {
        features: this.features,
        targets: this.targets,

        // model is the generated tree structure
        model: this._c45(data, target, features, featureTypes, 0),

        classify: function (sample) {
          return classify(this.model, sample)
        },

        toJSON: function() {
          return JSON.stringify(this.model)
        }
      };

      this.model = model.model

      callback(null, model);
    },

    getModel: function() {
      var classify = this.classify.bind(this)
      var model = this.model

      return {
        features: this.features,
        targets: this.targets,
        classify: function (sample) {
          return classify(model, sample)
        },
        toJSON: function() {
          return JSON.stringify(this.model)
        }
      }
    },

    _c45: function(data, target, features, featureTypes, depth) {
      var targets = unique(data.map(function(d) {
        return d[d.length-1];
      }));

      if (!targets.length) {
        return {
          type: 'result',
          value: 'none data',
          name: 'none data'
        };
      }

      if (targets.length === 1) {
        return {
          type: 'result',
          value: targets[0],
          name: targets[0]
        };
      }

      if (!features.length) {
        var topTarget = this.mostCommon(targets);
        return {
          type: 'result',
          value: topTarget,
          name: topTarget
        };
      }

      var bestFeatureData = this.maxGain(data, target, features, featureTypes);
      var bestFeature = bestFeatureData.feature;

      var remainingFeatures = features.slice(0);
      remainingFeatures.splice(features.indexOf(bestFeature), 1);

      if (featureTypes[this.features.indexOf(bestFeature)] === 'category') {
        var possibleValues = unique(data.map(function(d) {
          return d[this.features.indexOf(bestFeature)];
        }.bind(this)));
        var node = {
          name: bestFeature,
          type: 'feature_category',
          values: possibleValues.map(function(v) {
            var newData = data.filter(function(x) {
              return x[this.features.indexOf(bestFeature)] === v;
            }.bind(this));
            var childNode = {
              name: v,
              type: 'feature_value',
              child: this._c45(newData, target, remainingFeatures, featureTypes, depth+1)
            };
            return childNode;
          }.bind(this))
        };
      } else if (featureTypes[this.features.indexOf(bestFeature)] === 'number') {
        var possibleValues = unique(data.map(function(d) {
          return d[this.features.indexOf(bestFeature)];
        }.bind(this)));
        var node = {
          name: bestFeature,
          type: 'feature_number',
          cut: bestFeatureData.cut,
          values: []
        };

        var newDataRight = data.filter(function(x) {
          return parseFloat(x[this.features.indexOf(bestFeature)]) > bestFeatureData.cut;
        }.bind(this));
        var childNodeRight = {
          name: bestFeatureData.cut.toString(),
          type: 'feature_value',
          child: this._c45(newDataRight, target, remainingFeatures, featureTypes, depth+1)
        };
        node.values.push(childNodeRight);

        var newDataLeft = data.filter(function(x) {
          return parseFloat(x[this.features.indexOf(bestFeature)]) <= bestFeatureData.cut;
        }.bind(this));
        var childNodeLeft = {
          name: bestFeatureData.cut.toString(),
          type: 'feature_value',
          child: this._c45(newDataLeft, target, remainingFeatures, featureTypes, depth+1),
        };
        node.values.push(childNodeLeft);
      }
      return node;
    },

    maxGain: function(data, target, features, featureTypes) {
      var g45 = features.map(function(feature) {
        return this.gain(data, target, features, feature, featureTypes);
      }.bind(this));
      return max(g45, function(e) {
        return e.gain;
      });
    },

    gain: function(data, target, features, feature, featureTypes) {
      var setEntropy = this.entropy(data.map(function(d) {
        return d[d.length-1];
      }));
      if (featureTypes[this.features.indexOf(feature)] === 'category') {
        var attrVals = unique(data.map(function(d) {
          return d[this.features.indexOf(feature)];
        }.bind(this)));
        var setSize = data.length;
        var entropies = attrVals.map(function(n) {
          var subset = data.filter(function(x) {
            return x[feature] === n;
          });
          return (subset.length/setSize) * this.entropy(
            subset.map(function(d) {
              return d[d.length-1];
            })
          );
        }.bind(this));
        var sumOfEntropies = entropies.reduce(function(a, b) {
          return a + b;
        }, 0);
        return {
          feature: feature,
          gain: setEntropy - sumOfEntropies,
          cut: 0
        };
      } else if (featureTypes[this.features.indexOf(feature)] === 'number') {
        var attrVals = unique(data.map(function(d) {
          return d[this.features.indexOf(feature)];
        }.bind(this)));
        var gainVals = attrVals.map(function(cut) {
          var cutf = parseFloat(cut);
          var gain = setEntropy - this.conditionalEntropy(data, feature, cutf, target);
          return {
              feature: feature,
              gain: gain,
              cut: cutf
          };
        }.bind(this));
        var maxgain = max(gainVals, function(e) {
          return e.gain;
        });
        return maxgain;
      }
    },

    entropy: function(vals) {
      var uniqueVals = unique(vals);
      var probs = uniqueVals.map(function(x) {
        return this.prob(x, vals);
      }.bind(this));
      var logVals = probs.map(function(p) {
        return -p * this.log2(p);
      }.bind(this));
      return logVals.reduce(function(a, b) {
        return a + b;
      }, 0);
    },

    conditionalEntropy: function(data, feature, cut, target) {
      var subset1 = data.filter(function(x) {
        return parseFloat(x[this.features.indexOf(feature)]) <= cut;
      }.bind(this));
      var subset2 = data.filter(function(x) {
        return parseFloat(x[this.features.indexOf(feature)]) > cut;
      }.bind(this));
      var setSize = data.length;
      return subset1.length/setSize * this.entropy(
        subset1.map(function(d) {
          return d[d.length-1];
        })
      ) + subset2.length/setSize*this.entropy(
        subset2.map(function(d) {
          return d[d.length-1];
        })
      );
    },

    prob: function(target, targets) {
      return this.count(target,targets)/targets.length;
    },

    mostCommon: function(targets) {
      return sortBy(targets, function(target) {
        return this.count(target, targets);
      }.bind(this)).reverse()[0];
    },

    count: function(target, targets) {
      return targets.filter(function(t) {
        return t === target;
      }).length;
    },

    log2: function(n) {
      return Math.log(n) / Math.log(2);
    },

    toJSON: function() {
      return JSON.stringify({
        features: this.features,
        targets: this.targets,
        target: this.target,
        model: this.model,
      })
    },

    classify: function classify(model, sample) {
      // root is feature (attribute) containing all sub values
      var root = model;

      if (typeof root === 'undefined') {
        throw new Error('model is undefined')
      }

      while (root.type !== 'result') {
        var childNode;

        if (root.type === 'feature_number') {
          var featureName = root.name;
          var sampleVal = parseFloat(sample[featureName]);

          if (sampleVal <= root.cut) {
            childNode = root.values[1];
          } else {
            childNode = root.values[0];
          }
        } else {
          // feature syn attribute
          var feature = root.name;
          var sampleValue = sample[this.features.indexOf(feature)];

          // sub value , containing 2 childs
          childNode = find(root.values, function(x) {
            return x.name === sampleValue;
          });
        }

        // non trained feature
        if (typeof childNode === 'undefined') {
          return 'unknown';
        }
        root = childNode.child;
      }
      return root.value;
    },

    restore: function(options) {
      this.features = options.features || []
      this.targets = options.targets || ''
      this.target = options.target || ''
      this.model = options.model || {}
    }
  };

  if (typeof exports !== 'undefined') {
    if (typeof module !== 'undefined' && module.exports) {
      exports = module.exports = C45;
    }
    exports.C45 = C45;
  } else if (typeof define === 'function' && define.amd) {
    define([], function() {
      return C45;
    });
  } else {
    root.C45 = C45;
  }

})(this);


var Gpio = require('onoff').Gpio;

module.exports = function(dataPin, clkPin, csPin, numDevices) {

	const	OP_NOOP 	= 0x00,
			OP_DIGIT0 	= 0x01,
			OP_DIGIT1 	= 0x02,
			OP_DIGIT2 	= 0x03,
			OP_DIGIT3 	= 0x04,
			OP_DIGIT4 	= 0x05,
			OP_DIGIT5 	= 0x06,
			OP_DIGIT6 	= 0x07,
			OP_DIGIT7 	= 0x08,
			OP_DECODEMODE 	= 0x09,
			OP_INTENSITY 	= 0x0a,
			OP_SCANLIMIT 	= 0x0b,
			OP_SHUTDOWN 	= 0x0c,
			OP_DISPLAYTEST 	= 0x0f;

	const 	HIGH = 1,
			LOW = 0;
			LSBFIRST = 0,
			MSBFIRST = 1;
			/*
		   * The segments are identified as follows:
		   *    _a_
		   *  f|   |b
		   *   |_g_|
		   *   |   |
		   *  e|___|c  dp (decimal point)
		   *     d    * */

	const charTable = {
			'0' : parseInt('1111110',2),
			'1' : parseInt('0110000',2),
			'2' : parseInt('1101101',2),
			'3' : parseInt('1111001',2),
			'4' : parseInt('0110011',2),
			'5' : parseInt('1011011',2),
			'6' : parseInt('1011111',2),
			'7' : parseInt('1110000',2),
			'8' : parseInt('1111111',2),
			'9' : parseInt('1111011',2),
			'a' : parseInt('1110111',2),
			'b' : parseInt('0011111',2),
			'c' : parseInt('0001101',2),
			'd' : parseInt('0111101',2),
			'E' : parseInt('1001111',2),
			'e' : parseInt('1101111',2),
			'f' : parseInt('1000111',2),
			'g' : parseInt('1111011',2),
			'H' : parseInt('0110111',2),
			'h' : parseInt('0010111',2),
			'i' : parseInt('0010000',2),
			'j' : parseInt('0111100',2),
			'k' : parseInt('1010111',2),
			'l' : parseInt('0001110',2),
			'm' : parseInt('1110110',2),
			'n' : parseInt('0010101',2),
			'o' : parseInt('0011101',2),
			'p' : parseInt('1100111',2),
			'q' : parseInt('1110011',2),
			'r' : parseInt('0000101',2),
			's' : parseInt('1011011',2),
			't' : parseInt('0001111',2),
			'u' : parseInt('0011100',2),
			'v' : parseInt('0011100',2),
			'w' : parseInt('0011100',2),
			'x' : parseInt('0110111',2),
			'y' : parseInt('0111011',2),
			'z' : parseInt('1101101',2),
			'-' : parseInt('0000001',2),
			'_' : parseInt('0001000',2),
			'[' : parseInt('1001110',2),
			'(' : parseInt('1001110',2),
			']' : parseInt('1111000',2),
			')' : parseInt('1111000',2),
			'°' : parseInt('1100011',2),
			'\!' : parseInt('10100000',2),
			'\'' : parseInt('0100000',2),
			'\.' : parseInt('10000000',2),

			};

	if(typeof numDevices === 'undefined') {
		numDevices = 1;
	} else {
		if(typeof numDevices !== 'number' || numDevices<1 || numDevices>8) {
			throw 'numDevices must be a number between 1 and 8';
		}
	}

	var maxDevices = numDevices;

	var spiMosi= new Gpio(dataPin, 'out');
	var spiClk = new Gpio(clkPin, 'out');
	var spiCs  = new Gpio(csPin, 'out');

	// status is a pixelmap
	var status = new Array(64);

	// stores the bytes to send.
	var spidata = new Array(16);

	// CS enable pin goes low when transmitting
	spiCs.writeSync(HIGH);

	// clear the pixel data
	for(var i=0;i<64;i++){
		status[i]=0x00;
	}

	for(var i=0;i<numDevices;i++) {

		spiTransfer(i,OP_DISPLAYTEST,0);
		//scanlimit is set to max on startup
		setScanLimit(i,7);

		//decode is done in source
		spiTransfer(i,OP_DECODEMODE,0);
		clearDisplay(i);

		//wake up (shut down : false) on startup
		shutdown(i,false);
	}

	this.getDeviceCount = function() { return numDevices; };

	// shutdown puts it into power saving sleep mode.
	// call with false to wake it up.

	function shutdown(addr, b) {
	    if(addr<0 || addr>=maxDevices)
			throw 'address out of range';

	    if(b)
			spiTransfer(addr, OP_SHUTDOWN,0);
	    else
			spiTransfer(addr, OP_SHUTDOWN,1);
	}
	this.shutdown = shutdown;

	// sets the number of pixels per row, pretty much
	// always 8 for most devices.

	function setScanLimit(addr, limit) {
	    if(addr<0 || addr>=maxDevices)
			return;

		//console.log("OP_SCANLIMIT");
	    if(limit>=0 && limit<8)
	    	spiTransfer(addr, OP_SCANLIMIT,limit);
	}
	this.setScanLimit = setScanLimit;

	// sets the brightness of the LEDs, 0 to 15

	function setBrightness(addr, intensity) {
	    if(addr<0 || addr>=maxDevices)
			return;
	 	if (typeof intensity == 'undefined') return;

		intensity = constrain(intensity, 0, 15);

		spiTransfer(addr, OP_INTENSITY,intensity);

	}
	this.setBrightness = setBrightness;

	// clears the entire display

	function clearDisplay(addr) {
	    if(addr<0 || addr>=maxDevices)
			throw 'address out of range';

	    var offset;

	    offset=addr*8;
	    for(var i=0;i<8;i++) {
			status[offset+i]=0;
			spiTransfer(addr, i+1,status[offset+i]);
	    }
	}
    this.clearDisplay = clearDisplay;

	function setLed(addr, column, row, state) {

	    var offset;
	    var val=0x00;

	    if(addr<0 || addr>=maxDevices)
			throw 'address out of range';

	    if(row<0 || row>7 || column<0 || column>7)
			return;

	    offset=addr*8;

	    val=parseInt(10000000,2) >> column;

		if(state)
			status[offset+row]=status[offset+row] | val;
	    else {
			val=~val;
			status[offset+row]=status[offset+row] & val;
	    }

	    spiTransfer(addr, row+1,status[offset+row]);
	}
	this.setLed = setLed;

   /*
     * Display a hexadecimal digit on a 7-Segment Display
     * Params:
     * addr	address of the display
     * digit	the position of the digit on the display (0..7)
     * value	the value to be displayed. (0x00..0x0F)
     * dp	sets the decimal point.
     */
    function setDigit(addr, digit, value, dp){
	    if(addr<0 || addr>=maxDevices)
			throw 'address out of range';
		if(digit<0 || digit>7)
			throw 'invalid digit number';
		if(value<0 || value >15) throw 'number out of range';

		var offset = addr*8;
		var v = charTable[value];//.toString(16)];

		if(dp) v|=0x80; // set the decimal point bit if necessary

		status[offset+digit] = v;
		spiTransfer(addr, digit+1, v);

	};
	this.setDigit = setDigit;

	// pos is passed in as 0 to 7, 0 on the left, 7 on the right

	function showNumber(addr, num, decimalplaces, mindigits, leftjustified, pos, dontclear) {

	    if(addr<0 || addr>=maxDevices)
			throw 'address out of range';

		num = formatNumber(num, decimalplaces, mindigits);

		// internally, pos is 0 on the right, so we set defaults, and convert
		if(typeof pos === 'undefined') {
			if(leftjustified) {
				pos = 7;
			} else {
				pos = 0;
			}
		} else pos = 7-pos;

		// get rid of the decimal place but remember where it was
		var decimalplace;
		if(num.indexOf('.')<0) decimalplace = -1;
		else {
			decimalplace = num.length - num.indexOf('.') -1;
			num = num.split('.').join('');
		}
		if(leftjustified) {
			pos-=(num.length-1);
		}

		for(var i = 0; i<8; i++) {
			var offset = i+pos;
			var char = num.charAt(num.length-1-i);

			if((offset<8 && offset>=0) && (!dontclear || char!=''))
			{
				if(char=='-') setChar(addr, offset, char, i>0 && i==decimalplace);
				else setDigit(addr, offset, parseInt(char), i>0 && i==decimalplace);
			}
		}

	}
	this.showNumber = showNumber;

	function formatNumber(num, decimalplaces, mindigits) {
		var minus = false;

		if(num<0) {
			minus = true;
			num = Math.abs(num);
		}

		if(typeof decimalplaces !=='undefined') {
			num = num.toFixed(decimalplaces);
		} else {
			num = num.toString();
		}

		// if there's a decimal point then increase the mindigits by one
		// to compensate

		if(typeof mindigits ==='number') {
			if(num.indexOf('.')>=0) mindigits++;
			while(num.length<mindigits) num = '0'+num;
		}
		if(minus) num = '-'+num;
		return num;

	}

    /*
     */
    function setChar(addr, digit, char, dp){
		if(addr<0 || addr>=maxDevices)
			throw 'address out of range';
	    if(digit<0 || digit>7)
			throw 'invalid digit number';

		var offset = addr*8;
		var v = charTable[char] || charTable[char.toLowerCase()];//.toString(16)];

		if(dp) v|=0x80; // set the decimal point bit if necessary

		status[offset+digit] = v;
		spiTransfer(addr, digit+1, v);

	};
	this.setChar = setChar;

	function shiftOut(dataPin, clockPin, bitOrder, val)
	{

	     for (var i = 0; i < 8; i++)  {
	           if (bitOrder == LSBFIRST)
	                 dataPin.writeSync((val & (1 << i)) == 0 ? 0 : 1);
	           else
	                 dataPin.writeSync((val & (1 << (7 - i)))== 0 ? 0 : 1);

				clockPin.writeSync(HIGH);
				clockPin.writeSync(LOW);
	     }

	}
	function getBinaryString(val, minLength) {
		val = val.toString(2);
		while(val.length<minLength) val='0'+val;
		return val;
	}
	function constrain(value, min, max) {
		if(value<min) return min;
		else if(value>max) return max;
		else return value;
	}

	function spiTransfer(addr, opcode, data) {

		// the offset is the start row of data for this display
	    var offset=addr*2;
		// and the maximum number of bytes depends on the number
		// of displays
	    var maxbytes=maxDevices*2;

	    for(var i=0;i<maxbytes;i++)
			spidata[i]=0;

	    //put our device data into the array
	    spidata[offset+1]=opcode;
	    spidata[offset]=data;
	    //enable the line
	    spiCs.writeSync(LOW);
	    //Now shift out the data
		//console.log(spidata);

	    for(var i=maxbytes;i>0;i--) {
	 		shiftOut(spiMosi,spiClk, MSBFIRST, spidata[i-1]);
		}
	    //latch the data onto the display
	    spiCs.writeSync(HIGH);
	}
	this.spiTransfer = spiTransfer;
}


'use strict';

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.getServicesStatus = undefined;

var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; };

var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };

var _reduxActions = require('redux-actions');

var _debug = require('debug');

var _debug2 = _interopRequireDefault(_debug);

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

function _defineProperty(obj, key, value) { if (key in obj) { Object.defineProperty(obj, key, { value: value, enumerable: true, configurable: true, writable: true }); } else { obj[key] = value; } return obj; }

/**
 * Build a Redux compatible wrapper around a Feathers service.
 *
 * Instead of using a feathers-client service directly
 *     app.services('messages').create({ name: 'John' }, (err, data) => {...});
 * you first wrap the feathers service to expose Redux action creators and a reducer
 *     messages = reduxifyService(app, 'messages');
 * You can thereafter use the service in a standard Redux manner
 *     store.dispatch(messages.create({ name: 'John' }));
 * with async action creators being dispatched to a reducer which manages state.
 *
 * @param {Object} app the configured Feathers app, e.g. require('feathers-client')().configure(...)
 * @param {String} route is the Feathers' route for the service.
 * @param {String} name is the serviceName by which the service is known on client. Default route.
 * @param {Object} options
 * @returns {{find: *, get: *, create: *, update: *, patch: *, remove: *, on: *, reducer: *}}
 *
 * You will usually use on the client
 *      const users = reduxifyService(app, 'users');
 * However you may sometimes have awkward REST paths on the server like
 *      app.use('app.use('/verifyReset/:action/:value', ...);
 * You are then best of to use on the client
 *      const buildings = reduxifyService(app, '/verifyReset/:action/:value', 'verifyReset');
 * since you can thereafter use
 *      store.dispatch(verifyReset.create(...));
 *
 * Action creators for service calls are returned as { find, get, create, update, patch, remove }.
 * They expect the same parameters as their Feathers service methods, e.g. (id, data, params).
 *
 * Should you wish to write additional action creators, the { reducer } export expects action types
 *   'SERVICES_${SERVICE_NAME}_${METHOD}_PENDING', ...FULFILLED and ...REJECTED
 * where SERVICE_NAME is serviceName in upper case; METHOD is FIND, GET, ...
 *
 * Pro tip: You can implement optimistic updates within ...PENDING, finalizing them in ...FULFILL.
 *
 * The reducer's JS state (not immutable) is {
 *   isError: String|null,
 *   isLoading: Boolean,
 *   isSaving: Boolean,
 *   isFinished: Boolean,
 *   data: Object|null,
 *   queryResult: Object|null
 * }.
 * The find service call stores Feathers' query payload in queryResult. Other methods store in data.
 *
 * isError is Feathers' error payload { message, name, code, className, errors }.
 * If the feathers server response did not specify an error message, then the message property will
 * be feathers default of 'Error'.
 *
 * Options may change the state property names and the reducer action type names.
 *
 * Each service also gets a reset service call which re-initializes that service's state.
 * This may be used, for example, to remove isError in order to no longer render error messages.
 * store.dispatch(messages.reset(true)) will leave queryResult as is during initialization.
 *
 * An action creator for listening on service events is returned as { on } and could be used like:
 *   import feathersApp, { services } from './feathers';
 *   feathersApp.service('messages').on('created', data => { store.dispatch(
 *       services.messages.on('created', data, (event, data, dispatch, getState) => {
 *         // handle data change
 *       })
 *   ); });
 */

var reduxifyService = function reduxifyService(app, route) {
  var _handleActions;

  var name = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : route;
  var options = arguments.length > 3 && arguments[3] !== undefined ? arguments[3] : {};

  var debug = (0, _debug2.default)('reducer:' + name);
  debug('route ' + route);

  var defaults = {
    isError: 'isError',
    isLoading: 'isLoading',
    isSaving: 'isSaving',
    isFinished: 'isFinished',
    data: 'data',
    queryResult: 'queryResult',
    store: 'store',
    PENDING: 'PENDING',
    FULFILLED: 'FULFILLED',
    REJECTED: 'REJECTED'
  };
  var opts = Object.assign({}, defaults, options);
  var SERVICE_NAME = 'SERVICES_' + name.toUpperCase() + '_';

  var service = app.service(route);
  if (!service) {
    debug('redux: Feathers service \'' + route + ' does not exist.');
    throw Error('Feathers service \'' + route + ' does not exist.');
  }

  var reducerForServiceMethod = function reducerForServiceMethod(actionType, ifLoading, isFind) {
    var _ref;

    return _ref = {}, _defineProperty(_ref, actionType + '_' + opts.PENDING, function undefined(state, action) {
      var _extends2;

      debug('redux:' + actionType + '_' + opts.PENDING, action);
      return _extends({}, state, (_extends2 = {}, _defineProperty(_extends2, opts.isError, null), _defineProperty(_extends2, opts.isLoading, ifLoading), _defineProperty(_extends2, opts.isSaving, !ifLoading), _defineProperty(_extends2, opts.isFinished, false), _defineProperty(_extends2, opts.data, null), _defineProperty(_extends2, opts.queryResult, state[opts.queryResult] || null), _extends2));
    }), _defineProperty(_ref, actionType + '_' + opts.FULFILLED, function undefined(state, action) {
      var _extends3;

      debug('redux:' + actionType + '_' + opts.FULFILLED, action);
      return _extends({}, state, (_extends3 = {}, _defineProperty(_extends3, opts.isError, null), _defineProperty(_extends3, opts.isLoading, false), _defineProperty(_extends3, opts.isSaving, false), _defineProperty(_extends3, opts.isFinished, true), _defineProperty(_extends3, opts.data, !isFind ? action.payload : null), _defineProperty(_extends3, opts.queryResult, isFind ? action.payload : state[opts.queryResult] || null), _extends3));
    }), _defineProperty(_ref, actionType + '_' + opts.REJECTED, function undefined(state, action) {
      var _extends4;

      debug('redux:' + actionType + '_' + opts.REJECTED, action);
      return _extends({}, state, (_extends4 = {}, _defineProperty(_extends4, opts.isError, action.payload), _defineProperty(_extends4, opts.isLoading, false), _defineProperty(_extends4, opts.isSaving, false), _defineProperty(_extends4, opts.isFinished, true), _defineProperty(_extends4, opts.data, null), _defineProperty(_extends4, opts.queryResult, isFind ? null : state[opts.queryResult] || null), _extends4));
    }), _ref;
  };

  // ACTION TYPES

  var FIND = SERVICE_NAME + 'FIND';
  var GET = SERVICE_NAME + 'GET';
  var CREATE = SERVICE_NAME + 'CREATE';
  var UPDATE = SERVICE_NAME + 'UPDATE';
  var PATCH = SERVICE_NAME + 'PATCH';
  var REMOVE = SERVICE_NAME + 'REMOVE';
  var RESET = SERVICE_NAME + 'RESET';
  var STORE = SERVICE_NAME + 'STORE';

  return {
    // ACTION CREATORS
    // Note: action.payload in reducer will have the value of .data below
    find: (0, _reduxActions.createAction)(FIND, function (p) {
      return { promise: service.find(p), data: undefined };
    }),
    get: (0, _reduxActions.createAction)(GET, function (id, p) {
      return { promise: service.get(id, p) };
    }),
    create: (0, _reduxActions.createAction)(CREATE, function (d, p) {
      return { promise: service.create(d, p) };
    }),
    update: (0, _reduxActions.createAction)(UPDATE, function (id, d, p) {
      return { promise: service.update(id, d, p) };
    }),
    patch: (0, _reduxActions.createAction)(PATCH, function (id, d, p) {
      return { promise: service.patch(id, d, p) };
    }),
    remove: (0, _reduxActions.createAction)(REMOVE, function (id, p) {
      return { promise: service.remove(id, p) };
    }),
    reset: (0, _reduxActions.createAction)(RESET),
    store: (0, _reduxActions.createAction)(STORE, function (store) {
      return store;
    }),
    on: function on(event, data, fcn) {
      return function (dispatch, getState) {
        fcn(event, data, dispatch, getState);
      };
    },

    // REDUCER

    reducer: (0, _reduxActions.handleActions)(Object.assign({}, reducerForServiceMethod(FIND, true, true), reducerForServiceMethod(GET, true), reducerForServiceMethod(CREATE, false), reducerForServiceMethod(UPDATE, false), reducerForServiceMethod(PATCH, false), reducerForServiceMethod(REMOVE, false),

    // reset status if no promise is pending
    _defineProperty({}, RESET, function (state, action) {
      var _extends5;

      debug('redux:' + RESET, action);

      if (state[opts.isLoading] || state[opts.isSaving]) {
        return state;
      }

      return _extends({}, state, (_extends5 = {}, _defineProperty(_extends5, opts.isError, null), _defineProperty(_extends5, opts.isLoading, false), _defineProperty(_extends5, opts.isSaving, false), _defineProperty(_extends5, opts.isFinished, false), _defineProperty(_extends5, opts.data, null), _defineProperty(_extends5, opts.queryResult, action.payload ? state[opts.queryResult] : null), _defineProperty(_extends5, opts.store, null), _extends5));
    }),

    // update store
    _defineProperty({}, STORE, function (state, action) {
      debug('redux:' + STORE, action);

      return _extends({}, state, _defineProperty({}, opts.store, action.payload));
    })), (_handleActions = {}, _defineProperty(_handleActions, opts.isError, null), _defineProperty(_handleActions, opts.isLoading, false), _defineProperty(_handleActions, opts.isSaving, false), _defineProperty(_handleActions, opts.isFinished, false), _defineProperty(_handleActions, opts.data, null), _defineProperty(_handleActions, opts.queryResult, null), _defineProperty(_handleActions, opts.store, null), _handleActions))
  };
};

/**
 * Convenience method to build wrappers for multiple services. You should this not reduxifyService.
 *
 * @param {Object} app - See reduxifyService
 * @param {Object|Array|String} routeNameMap - The feathers services to reduxify. See below.
 * @param {Object} options - See reduxifyService
 * @returns {Object} Each services' action creators. See reduxifyService.
 *
 * If the feathers server has:
 *   app.use('users', ...);
 *   app.use('/buildings/:buildingid', ...);
 * then you can do
 *   services = reduxifyServices(app, { users: 'users', '/buildings/:buildingid': 'buildings' });
 *   ...
 *   store.dispatch(users.create(...));
 *   store.dispatch(users.create(...));
 *
 * A routeNameMap of ['users', 'members'] is the same as { users: 'users', members: 'members' }.
 * A routeNameMao of 'users' is the same as { users: 'users' }.
 */

exports.default = function (app, routeNameMap, options) {
  var services = {};
  var routeNames = {};

  if (typeof routeNameMap === 'string') {
    routeNames = _defineProperty({}, routeNameMap, routeNameMap);
  } else if (Array.isArray(routeNameMap)) {
    routeNameMap.forEach(function (name) {
      routeNames[name] = name;
    });
  } else if ((typeof routeNameMap === 'undefined' ? 'undefined' : _typeof(routeNameMap)) === 'object') {
    routeNames = routeNameMap;
  }

  Object.keys(routeNames).forEach(function (route) {
    services[routeNames[route]] = reduxifyService(app, route, routeNames[route], options);
  });

  return services;
};

/**
 * Get a status to display as a summary of all Feathers services.
 *
 * The services are checked in serviceNames order.
 * The first service with an error message, returns that as the status.
 * Otherwise the first service loading or saving returns its status.
 *
 * @param {Object} servicesState - the slice of state containing the states for the services.
 *    state[name] has the JS state (not immutable) for service 'name'.
 * @param {Array|String} serviceNames
 * @returns {{message: string, className: string, serviceName: string}}
 *    message is the English language status text.
 *    You can create your own internationalized messages with serviceName and className.
 *    className will be isLoading, isSaving or it will be Feathers' error's className.
 */

var getServicesStatus = exports.getServicesStatus = function getServicesStatus(servicesState, serviceNames) {
  var status = { // eslint-disable-line no-var
    message: '',
    className: '',
    serviceName: ''
  };
  serviceNames = // eslint-disable-line no-param-reassign
  Array.isArray(serviceNames) ? serviceNames : [serviceNames];

  // Find an error with an err.message. 'Error' is what feather returns when there is no msg text.
  var done = serviceNames.some(function (name) {
    var state = servicesState[name];

    if (state && state.isError && state.isError.message && state.isError.message !== 'Error') {
      status.message = name + ': ' + state.isError.message;
      status.className = state.isError.className;
      status.serviceName = name;
      return true;
    }

    return false;
  });

  if (done) {
    return status;
  }

  serviceNames.some(function (name) {
    var state = servicesState[name];

    if (state && !state.isError && (state.isLoading || state.isSaving)) {
      status.message = name + ' is ' + (state.isLoading ? 'loading' : 'saving');
      status.className = state.isLoading ? 'isLoading' : 'isSaving';
      status.serviceName = name;
      return true;
    }

    return false;
  });

  return status;
};


'use strict';

module.exports = exports = getExampleCode;

/**
 * Compiles down an `example`. Async examples call an imaginary `done` function
 * once they're done.
 *
 * @param {Object} comment
 * @param {String} comment.testCase
 * @param {String} comment.expectedResult
 * @param {Boolean} comment.isAsync
 * @return {String}
 */

function getExampleCode(comment) {
  var expectedResult = comment.expectedResult;
  var isAsync = comment.isAsync;
  var testCase = comment.testCase;

  if(isAsync) {
    return '\nfunction cb(err, result) {' +
      'if(err) return done(err);' +
      'result.should.eql(' + expectedResult + ');' +
      'done();' +
    '}\n' +
    'var returnValue = ' + testCase + ';' +
    'if(returnValue && returnValue.then && typeof returnValue.then === \'function\') {' +
      'returnValue.then(cb.bind(null, null), cb);' +
    '}';
  } else {
    return '(' + testCase + ').should.eql(' + expectedResult + ');';
  }
}

/**
 * Escapes a string for meta-quoting
 */

function escapeString(str) {
  return str.replace(/\\/g, '\\\\').replace(/\'/g, '\\\'');
}

exports.escapeString = escapeString;


'use strict';

/**
 * @file
 *
 * Copyright 2018 FutoIn Project (https://futoin.org)
 * Copyright 2018 Andrey Galkin <andrey@futoin.org>
 *
 * 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.
 */

const { createHash, createHmac } = require( 'crypto' );

/**
 * Get expected hash length.
 *
 * @func
 * @alias hkdf.hash_length
 * @param {string} hash - Hash algorithm
 * @returns {integer} hash digest byte length
 *
 * @note Values are hardcoded with fallback for unknown algorithms.
 */
const hash_length = ( hash ) => {
    switch ( hash ) {
    case 'sha256': return 32;
    case 'sha512': return 64;
    case 'sha224': return 28;
    case 'sha384': return 48;
    // Not yet supported by Node.js crypto
    // case 'blake2s256': return 32;
    // case 'blake2b512': return 64;
    case 'sha1': return 20;
    case 'md5': return 16;
    // Not yet supported by Node.js crypto
    // case 'gost': return 32;
    default: return createHash( hash ).digest().length;
    }
};

/**
 * HKDF extract action.
 *
 * @func
 * @alias hkdf.extract
 * @param {string} hash - Hash algorithm
 * @param {integer} hash_len - Hash digest length
 * @param {Buffer|string} ikm - Initial Keying Material
 * @param {Buffer|string} salt - Optional salt (recommended)
 * @returns {Buffer} A buffer with pseudorandom key
 *
 * @note Values are hardcoded with fallback for unknown algorithms.
 */
const hkdf_extract = ( hash, hash_len, ikm, salt ) => {
    const b_ikm = Buffer.isBuffer( ikm ) ? ikm : Buffer.from( ikm );
    const b_salt = ( salt && salt.length ) ? Buffer.from( salt ) : Buffer.alloc( hash_len, 0 );

    return createHmac( hash, b_salt ).update( b_ikm ).digest();
};

/**
 * HKDF expand action.
 *
 * @func
 * @alias hkdf.expand
 * @param {string} hash - Hash algorithm
 * @param {integer} hash_len - Hash digest length
 * @param {Buffer|string} prk - A buffer with pseudorandom key
 * @param {Buffer|string} length - length of output keying material in octets
 * @param {Buffer|string} info - Optional context (safe to skip)
 * @returns {Buffer} A buffer with output keying material
 *
 * @note Values are hardcoded with fallback for unknown algorithms.
 */
const hkdf_expand = ( hash, hash_len, prk, length, info ) => {
    const b_info = Buffer.from( info || '' );
    const info_len = b_info.length;

    const steps = Math.ceil( length / hash_len );

    if ( steps > 0xFF ) {
        throw new Error( `OKM length ${length} is too long for ${hash} hash` );
    }

    // use single buffer with unnecessary create/copy/move operations
    const t = Buffer.alloc( hash_len * steps + info_len + 1 );

    for ( let c = 1, start = 0, end = 0; c <= steps; ++c ) {
        // add info
        b_info.copy( t, end );
        // add counter
        t[ end + info_len ] = c;

        createHmac( hash, prk )
            // use view: T(C) = T(C-1) | info | C
            .update( t.slice( start, end + info_len + 1 ) )
            .digest()
            // put back to the same buffer
            .copy( t, end );

        start = end; // used for T(C-1) start
        end += hash_len; // used for T(C-1) end & overall end
    }

    return t.slice( 0, length );
};

/**
 * HMAC-based Extract-and-Expand Key Derivation Function (HKDF)
 *
 * @param {Buffer|string} ikm - Initial Keying Material
 * @param {integer} length - Required byte length of output
 * @param {Buffer|string} salt='' - Optional salt (recommended)
 * @param {Buffer|string} info='' - Optional context (safe to skip)
 * @param {string} hash='SHA-256' - HMAC hash function to use
 * @returns {Buffer} Raw buffer with derived key of @p length bytes
 */
function hkdf( ikm, length, { salt='', info='', hash='SHA-256' } = {} ) {
    hash = hash.toLowerCase().replace( '-', '' );

    // 0. Hash length
    const hash_len = hash_length( hash );

    // 1. extract
    const prk = hkdf_extract( hash, hash_len, ikm, salt );

    // 2. expand
    return hkdf_expand( hash, hash_len, prk, length, info );
}

Object.defineProperties( hkdf, {
    hash_length : {
        configurable: false,
        enumerable: false,
        writable: false,
        value: hash_length,
    },
    extract : {
        configurable: false,
        enumerable: false,
        writable: false,
        value: hkdf_extract,
    },
    expand : {
        configurable: false,
        enumerable: false,
        writable: false,
        value: hkdf_expand,
    },
} );

module.exports = hkdf;


/*global Buffer */

var http  = require('http'),
    https = require('https'),
    Url   = require('url'),
    EventEmitter = require('events').EventEmitter,
    querystring = require('querystring');

// Handles changes made in node v0.3.0
var NOT_FOUND_ERR_NO = process.ENOENT ? process.ENOENT : require('constants').ENOENT;
var MAX_DOCS = 1000; // The maximum number of docs to send in a single batch

function noOp(err) { if (err) { throw err; } }

var CONNECTION_DEFAULTS = {
  host: '127.0.0.1:5984',
  port: 5984,
  hostname: '127.0.0.1',
  pathname: "/"
};

function CouchClient(url) {
  var uri = Url.parse(url);
  uri.secure = uri.protocol == 'https:';
  uri.protocolHandler = uri.secure ? https : http;
  uri.__proto__ = CONNECTION_DEFAULTS;
  var revCache = {};

  // A simple wrapper around node's http(s) request.
  function request(method, path, body, callback) {
    var stream;
    // Body is optional
    if (typeof body === 'function' && typeof callback === 'undefined') {
      callback = body;
      body = undefined;
    }
    // Return a stream if no callback is specified
    if (!callback) {
      stream = new EventEmitter();
      stream.setEncoding = function () {
        throw new Error("This stream is always utf8");
      };
    }

    function errorHandler(err) {
      if (callback) { callback(err); }
      if (stream) { stream.emit('error', err); }
    }

    var headers = {
      "Host": uri.host
    };
    // add the authorization header if provided and using https
    if (uri.auth) {
      headers["Authorization"] = "Basic " + new Buffer(uri.auth, "ascii").toString("base64");
    }

    if (body) {
      body = JSON.stringify(body);
      headers["Content-Length"] = Buffer.byteLength(body);
      headers["Content-Type"] = "application/json";
    }

    var options = {
      host: uri.hostname,
      method: method,
      path: path,
      port: uri.port,
      headers: headers
    };
    var request = uri.protocolHandler.request(options, function (response) {
      response.setEncoding('utf8');
      var body = "";
      response.on('data', function (chunk) {
        if (callback) { body += chunk; }
        if (stream) { stream.emit('data', chunk); }
      });
      response.on('end', function () {
        if (callback) {
          try {
            var parsedBody = JSON.parse(body);
            callback(null, parsedBody);
          } catch(err) {
            callback(err);
          }
        }
        if (stream) { stream.emit('end'); }
      });
      response.on('error', errorHandler);
    });
    request.on('error', errorHandler);

    if (body) { request.write(body, 'utf8'); }
    request.end();

    return stream;
  }

  // Requests UUIDs from the couch server in tick batches
  var uuidQueue = [];
  function getUUID(callback) {
    uuidQueue.push(callback);
    if (uuidQueue.length > 1) { return; }
    function consumeQueue() {
      var pending = uuidQueue.splice(0, MAX_DOCS);
      if (uuidQueue.length) { process.nextTick(consumeQueue); }
      // console.log("Bulk getting UUIDs %s", pending.length);
      request("GET", "/_uuids?count=" + pending.length, function (err, result) {
        if (err) {
          pending.forEach(function (callback) {
            callback(err);
          });
          return;
        }
        if (result.uuids.length !== pending.length) {
          throw new Error("Wrong number of UUIDs generated " + result.uuids.length + " != " + pending.length);
        }
        result.uuids.forEach(function (uuid, i) {
          pending[i](null, uuid);
        });
      });
    }
    process.nextTick(consumeQueue);
  }

  // Saves documents in batches
  var saveValues = [];
  var saveQueue = [];
  function realSave(doc, callback) {
    // Put key and rev on the value without changing the original
    saveValues.push(doc);
    saveQueue.push(callback);
    if (saveQueue.length > 1) { return; }
    function consumeQueue() {
      var pending = saveQueue.splice(0, MAX_DOCS);
      var body = saveValues.splice(0, MAX_DOCS);
      if (saveQueue.length) { process.nextTick(consumeQueue); }
      // console.log("Bulk saving %s", body.length);
      request("POST", uri.pathname + "/_bulk_docs", {docs: body}, function (err, results) {
        if (results.error) {
          err = new Error("CouchDB Error: " + JSON.stringify(results));
          if (results.error === 'not_found') { err.errno = NOT_FOUND_ERR_NO; }
        }
        if (err) {
          pending.forEach(function (callback) {
            callback(err);
          });
          return;
        }
        results.forEach(function (result, i) {
          var doc = body[i];
          doc._id = result.id;
          doc._rev = result.rev;
          revCache[result.id] = result.rev;
          pending[i](null, doc);
        });
      });
    }
    process.nextTick(consumeQueue);
  }

  var getQueue = [];
  var getKeys = [];
  function realGet(key, includeDoc, callback) {
    getKeys.push(key);
    getQueue.push(callback);
    if (getQueue.length > 1) { return; }
    function consumeQueue() {
      var pending = getQueue.splice(0, MAX_DOCS);
      var keys = getKeys.splice(0, MAX_DOCS);
      if (getQueue.length) { process.nextTick(consumeQueue); }
      var path = uri.pathname + "/_all_docs";
      if (includeDoc) { path += "?include_docs=true"; }
      // console.log("Bulk Getting %s documents", keys.length);
      request("POST", path, {keys: keys}, function (err, results) {
        if (!results.rows) {
          err = new Error("CouchDB Error: " + JSON.stringify(results));
        }
        if (err) {
          pending.forEach(function (callback) {
            callback(err);
          });
          return;
        }
        results.rows.forEach(function (result, i) {
          var err;
          if (includeDoc) {
            if (result.error) {
              err = new Error("CouchDB Error: " + JSON.stringify(result));
              if (result.error === 'not_found') { err.errno = NOT_FOUND_ERR_NO; }
              pending[i](err);
              return;
            }
            if (!result.doc) {
              err = new Error("Document not found for " + JSON.stringify(result.key));
              err.errno = NOT_FOUND_ERR_NO;
              pending[i](err);
              return;
            }
            pending[i](null, result.doc);
            return;
          }
          pending[i](null, result.value);
        });
      });
    }
    process.nextTick(consumeQueue);
  }

  function save(doc, callback) {
    if (!callback) { callback = noOp; }
    if (doc._id) {
      if (!doc._rev) {
        if (!revCache.hasOwnProperty(doc._id)) {
          realGet(doc._id, false, function (err, result) {
            if (err) { return callback(err); }
            if (result) {
              revCache[doc._id] = result.rev;
              doc._rev = result.rev;
            }
            realSave(doc, callback);
          });
          return;
        }
        doc._rev = revCache[doc._id];
      }
    }
    realSave(doc, callback);
  }

  function get(key, callback) {
    realGet(key, true, callback);
  }

  function remove(doc, callback) {
    if (typeof doc === 'string') {
      doc = {_id: doc};
    }
    doc._deleted = true;
    save(doc, callback);
  }

  function changes(since, callback) {
    var stream = request("GET", uri.pathname + "/_changes?feed=continuous&heartbeat=1000&since=" + since);
    var data = "";
    function checkData() {
      var p = data.indexOf("\n");
      if (p >= 0) {
        var line = data.substr(0, p).trim();
        data = data.substr(p + 1);
        if (line.length) {
          callback(null, JSON.parse(line));
        }
        checkData();
      }
    }
    stream.on('error', callback);
    stream.on('data', function (chunk) {
      data += chunk;
      checkData();
    });
    stream.on('end', function () {
      throw new Error("Changes feed got broken!");
    });
  }

  function view(viewName, obj, callback) {
	var method = "GET";
	var body = null;
    if (typeof obj === 'function') {
      callback = obj;
      obj = null;
    }

    if ( viewName.substr(0,1) != '/' ) {
		// assume viewname is designdocname/viewname
		var parts = viewName.split("/",2);
		if ( parts.length == 2 ) {
			viewName = uri.pathname+"/_design/"+parts[0]+"/_view/"+parts[1];
		}
	}

    if (obj && typeof obj === 'object') {
      Object.keys(obj).forEach(function(key){
		if ( key === 'keys' ) {
			body = { keys: obj[key] }; // body is json stringified in request fn
			method='POST';
		} else {
			obj[key] = JSON.stringify(obj[key]);
		}
      });
      var getParams = querystring.stringify(obj);
      if (getParams){
        viewName = viewName + '?' + getParams;
      }
    }
    request(method, viewName, body, callback);
  }

  // Expose the public API
  return {
    get: get,
    save: save,
    remove: remove,
    changes: changes,
    request: request,
    uri: uri,
    view: view
  };
}

module.exports = CouchClient;