const CacheInterface = require('./Cache.js');
// const fs = require('fs');
const Path = require('path');
const Uuid = require('uuid/v4');
const Volatile = require('volatile');
const {Entity, ApexEntity, Instance} = require('./Entity.js');

module.exports = function xGraph(__options = {}) {
	this.__options = __options;
	let eventListeners = {
		exit: [],
		setup: [],
		start: [],
		run: []
	};

	let dispatchEvent = this.dispatchEvent = function dispatchEvent(eventName, options) {
		for (let callback of eventListeners[eventName]) {
			callback(options);
		}
	};

	this.on = function on(eventName, listener) {
		eventListeners[eventName].push(listener);
	};

	this.boot = function boot() {
		return (async function (__options) {
			const log = checkFlag('logger')?__options.logger: (require('../lib/Logger.js'))(__options);
			let cacheInterface;
			let ModCache = {};					// {<folder>: <module>}
			let Stop = {};						// {<Apex pid>:<function>}
			let EntCache = {};					// {<Entity pid>:<Entity>
			let ImpCache = {};					// {<Implementation path>: <Implementation(e.g. disp)>}
			let Nxs = {
				genPid,
				genModule,
				addModule,
				genEntity,
				deleteEntity,
				saveEntity,
				getFile,
				GetModule,
				loadDependency,
				sendMessage,
				exit,
			};

			function checkFlag(flag) {
				// console.dir(__options);
				return flag in __options && __options[flag];
			}

			function createRequireFromModuleType(modtype) {
				return cacheInterface.loadDependency.bind(cacheInterface, modtype);
			}

			function indirectEvalImp(entName, entString, ...injections) {
				let _eval = _ => {
					return (1,eval)(_);
				};

				let container = `//# sourceURL=${entName}\n(function(log, require) {const a = ${entString}; return a; })`;
				let imp = _eval(container);
				imp = imp(...injections);

				if(typeof imp == 'undefined') {
					if (!('dispatch' in imp)) {
						log.e('Invalid Entity File');
						throw new Error('E_INVALID_ENTITY');
					}
				}

				else if(typeof imp === 'function') return imp;

				//we have a dispatch tabler! (this is either the iife or pseudo class pattern)
				else if(typeof imp === 'object') {
					log.w('IIFE entity definitions are deprecated');
					log.w(`upgrade ${entName} to a class definition`);

					if (!('dispatch' in imp)) {
						log.e('Entity does not return a dispatch Table');
						throw new Error('E_NO_DISPATCH_TABLE');
					}
					let temp = function() {};
					temp.prototype = imp.dispatch;
					imp = temp;
					return imp;
				}

				else {
					log.e('Not even jesus could tell you what went wrong here');
					throw new Error('E_ENTITY_NEEDS_JESUS');
				}

			}

			log.i('=================================================');
			log.i('Nexus Warming Up:');

			//set CWD
			__options.cwd = __options.cwd ? Path.resolve(__options.cwd) : Path.resolve('.');

			//set Cache location
			__options.cache = __options.cache || Path.join(__options.cwd, 'cache');
			log.v(`cache set to ${__options.cache}`);

			await initiate();

			let externalPid = genPid();
			return {
				send: function send(com, pid, fun) {
					// log.v(com, pid);
					if (!('Passport' in com))
						com.Passport = {};
					com.Passport.To = pid;
					com.Passport.Apex = externalPid;
					if (fun)
						com.Passport.From = externalPid;
					if (!('Pid' in com.Passport))
						com.Passport.Pid = genPid();

					sendMessage(com, fun);
				},
				stop: exit,
				Pid: externalPid,
				Apex: externalPid
			};

			////////////////////////////////////////////////////////////////////////////////////////////////
			//
			// Only Function Definitions Beyond This Point
			//
			//

			/**
			 *  The main process of starting an xGraph System.
			 */
			async function initiate() {
				log.i('--Nexus/Initiate');
				let Setup = {};
				let Start = {};
				cacheInterface = new CacheInterface({
					path: __options.cache,
					log
				});

				let cache = await cacheInterface.loadCache();
				Start = cache.start;
				Setup = cache.setup;
				Stop = Object.assign(Stop, cache.stop);

				await setup();

				await start();

				run();

				/////////////////////////////////////////////////////////////////////////////////////////
				//
				// Only Helper Functions Beyond This Point
				//
				//

				/**
				 * Call setup on the required Module Apexes
				 */
				async function setup() {
					log.i('--Nexus/Setup');
					//build the setup promise array
					let setupArray = [];

					for (let pid in Setup) {
						setupArray.push(new Promise((resolve, _reject) => {
							let com = {};
							com.Cmd = Setup[pid];
							com.Passport = {};
							com.Passport.To = pid;
							com.Passport.Pid = genPid();
							sendMessage(com, resolve);
						}));
					}

					await Promise.all(setupArray);
					log.v('--Nexus: All Setups Complete');

				}

				/**
				 * Call Start on the required Module Apexes
				 */
				async function start() {
					log.i('--Nexus/Start');
					//build the setup promise array
					let startArray = [];

					for (let pid in Start) {
						startArray.push(new Promise((resolve, _reject) => {
							let com = {};
							com.Cmd = Start[pid];
							com.Passport = {};
							com.Passport.To = pid;
							com.Passport.Pid = genPid();
							sendMessage(com, resolve);
						}));
					}

					await Promise.all(startArray);
					log.v('--Nexus: All Starts Complete');
				}

				/**
				 * Send Finished command if the process was generated
				 */
				function run() {
					log.i('--Nexus/Run');
					if ('send' in process) {
						process.send('{"Cmd":"Finished"}');
					}
				}
			}

			//
			//
			// Helper Functions as well as Entity definition
			//
			//

			/**
			 * @description stop the system by calling all stop methods and
			 * releasing all entities from memory
			 * exit code 0 will gracefully exit,
			 * @param {number} [code=0] the code to exit with
			 */
			async function exit(code = 0) {
				log.i('--Nexus/Stop');
				//build the Stop promise array
				let stopTasks = [];

				log.i('Nexus unloading node modules');
				log.v(Object.keys(require.cache).join('\n'));

				for (let pid in Stop) {
					stopTasks.push(new Promise((resolve, _reject) => {
						let com = {};
						com.Cmd = Stop[pid];
						com.Passport = {};
						com.Passport.To = pid;
						com.Passport.Pid = genPid();
						sendMessage(com, resolve);
					}));
				}
				await Promise.all(stopTasks);
				log.v('--Nexus: All Stops Complete');

				dispatchEvent('exit', { exitCode: code });
			}

			/**
			 * generate a 32 character hex pid
			 */
			function genPid() {
				let str = Uuid();
				let pid = str.replace(/-/g, '').toUpperCase();
				return pid;
			}

			/**
			 * Send a message from an entity to an Apex entity.
			 * If a callback is provided, return when finished
			 * @param {object} com 			the message object
			 * @param {string} com.Cmd 		the command of the message
			 * @param {object} com.Passport	the information about the message
			 * @param {string} com.Passport.To the Pid of the recipient module
			 * @param {string} com.Passport.Pid the ID of the message
			 * @param {string=} com.Passport.From the Pid of the sending module
			 * @callback fun 				the callback function to return to when finished
			 */
			function sendMessage(com, fun = _ => _) {
				if (!('Passport' in com)) {
					log.w('Message has no Passport, ignored');
					log.w('    ' + JSON.stringify(com));
					fun('No Passport');
					return;
				}
				if (!('To' in com.Passport) || !com.Passport.To) {
					log.w('Message has no destination entity, ignored');
					log.w('    ' + JSON.stringify(com));
					fun('No recipient in message', com);
					return;
				}
				if (!('Pid' in com.Passport)) {
					log.w('Message has no message id, ignored');
					log.w('    ' + JSON.stringify(com));
					fun('No message id', com);
					return;
				}

				let pid = com.Passport.To;
				let apx = com.Passport.Apex || pid;
				if (pid in EntCache) {
					done(null, EntCache[pid]);
					return;
				} else {
					getEntityContext(pid, done);
				}

				async function done(err, entContextVolatile) {
					let entApex = await new Promise(res =>
						entContextVolatile.lock((val) => {
							res(val.Apex);
							return val;
						})
					);

					if (err) {
						log.w(err);
						log.w(JSON.stringify(com, null, 2));
						fun(err, com);
						return;
					}

					//TODO pid instanceOf ApexEntity
					if ((EntCache[pid].Apex == EntCache[pid].Pid) || (entApex == apx)) {
						let entContext = await new Promise(res => entContextVolatile.lock((context) => {
							res(context); return context;
						}));
						entContext.instance.dispatch(com, reply);
					} else {
						let err = 'Trying to send a message to a non-Apex'
							+ 'entity outside of the sending module';
						log.w(err);
						log.w(JSON.stringify(com, null, 2));
						fun(err, com);
					}
				}
				function reply(err, q) {
					// log.i('NEXUS MESSAGE:', com)
					fun(err, q);
				}
			}

			/**
			 * Creates an Entity in the module, which is defined by the apx, from the given entity definition
			 * The entity is then stored in EntCache (the location of all "in Memory" entities)
			 * @param {string} apx 			the Pid of the module Apex in which this entity will be generated
			 * @param {object} par 			the Par of the entity that will be created
			 * @param {string} par.Entity 	The entity type that will be generated
			 * @param {string=} par.Pid		the pid to define as the pid of the entity
			 * @return {pid} par.Pid		the pid of the generated entity
			 * @callback fun
			 */
			async function genEntity(par, fun = _ => log.e(_)) {
				if (!('Entity' in par)) {
					fun('No Entity defined in Par');
					return;
				}
				par.Pid = par.Pid || genPid();

				let impkey = (par.Module + '/' + par.Entity);
				let mod = ModCache[par.Module];

				if (!(par.Entity in mod.files)) {
					log.e('<' + par.Entity + '> not in module <' + par.Module + '>');
					fun('Null entity');
					return;
				}

				if (!(impkey in ImpCache)) {
					let entString = await new Promise(async (res, _rej) => {
						mod.file(par.Entity).async('string').then((string) => res(string));
					});
					ImpCache[impkey] = indirectEvalImp(impkey, entString, log,
						createRequireFromModuleType(par.Module));
				}

				EntCache[par.Pid] = new Volatile(new Entity(Nxs, ImpCache[impkey], par, log));
				fun(null, par.Pid);
			}

			/**
			 * Delete an entity from the module's memory.  If the entity is an Apex of a Module,
			 * then delete all the entities found in that module as well.
			 * @param {string} pid 		the pid of the entity
			 * @callback fun  			the callback to return the pid of the generated entity to
			 */
			function deleteEntity(pid, fun = (err, _pid) => { if (err) log.e(err); }) {
				cacheInterface.deleteEntity(pid, (err, removedPidArray) => {

					//remove ent from EntCache (in RAM)
					for (let i = 0; i < removedPidArray.length; i++) {
						let entPid = removedPidArray[i];
						if (entPid in EntCache) {
							delete EntCache[entPid];
						}
					}
					log.v(`Removed ${(removedPidArray.length == 1) ? 'Entity' : 'Entities'
					} ${removedPidArray.join(' ')}`);
					fun(err, pid);
				});
			}

			/**
			 * Save an entity file. Make sure that all nested files exist in the
			 * cache prior to saving said file
			 * @param {object} par 		the par of the entity
			 * @callback fun  			the callback to return the pid of the generated entity to
			 */
			async function saveEntity(par, fun = (err, _pid) => { if (err) log.e(err); }) {
				let saveEntity = (async (par) => {
					await new Promise((res, rej) => {
						cacheInterface.saveEntityPar(par, (err, pid) => {
							if (err){
								log.e(err, 'saving ', pid);
								rej(err);
							}
							log.v(`Saved entity ${par.Pid}`);
							res();
						});
					});
				});

				//check if the entity is the modules Apex
				if (par.Pid != par.Apex) {
					//check if the Apex exists in the cache
					cacheInterface.getEntityPar(par.Apex, async (err) => {
						if (err) {

							//get the Apex's par from the EntCache
							let apexPar = await new Promise((res, _rej) => {
								EntCache[par.Apex].lock((entityContext) => {
									res(entityContext.Par);
									return entityContext;
								});
							});

							log.v('Must first save the Apex -- Saving...');
							await saveEntity(apexPar);
							await saveEntity(par);
							fun(null, par.Pid);
						} else {
							//this entity is not the apex and the apex is alread in the cache
							await saveEntity(par);
							fun(null, par.Pid);
						}
					});
				} else {
					await saveEntity(par);
					fun(null, par.Pid);
				}
			}

			/**
			 * Add a module into the in memory Module Cache (ModCache)
			 * @param {string} modName 		the name of the module
			 * @param {string} modZip 		the zip of the module
			 * @callback fun 							the callback just returns the name of the module
			 */
			async function addModule(modName, modZip, fun) {
				//modZip is the uint8array that can be written directly to the cache directory
				if (checkFlag('allow-add-module')) {
					try {
						let newModname = await cacheInterface.addModule(modName, modZip);
						fun(null, newModname);
					} catch(e) {
						fun(e, modName);
					}
				} else {
					let err = 'addModule not permitted in current xGraph process\n'
						+'run xgraph with --allow-add-module to enable';
					log.w(err);
					fun(err);
				}
			}

			/**
			 * Access a file that exists in the module.json
			 * @param {string} module 		the module to look for the file in
			 * @param {string} filename 	the name of the file we're looking for
			 * @callback fun				the callback to return the pid of the generated entity to
			 */
			function getFile(module, filename, fun = _ => _) {
				let mod = ModCache[module];
				if (filename in mod.files) {
					mod.file(filename).async('string').then((dat) => {
						fun(null, dat);
					});
					return;
				}
				let err = `Error: File ${filename} does not exist in module ${module}`;
				log.e(err);
				fun(err);
			}

			/**
			 * load a dependency for a module
			 * @param {string} apx 		pid of the entities apex
			 * @param {string} pid 		the pid of the entity
			 * @param {string} str 			the string of the module to require
			 */
			function loadDependency(moduleType, pid, str) {
				return cacheInterface.loadDependency(moduleType, str);
			}

			/**
			 * Spin up an entity from cache into memory and retrievd its context
			 * otherwise just return it's context from memory
			 * @param {string} apx 		the pid of the entities apex
			 * @param {string} pid 		the pid of the entity
			 * @callback fun  			the callback to return the pid of the generated entity to
			 */
			async function getEntityContext(pid, fun = _ => _) {
				EntCache[pid] = new Volatile({});
				await EntCache[pid].lock((_entityContext) => {
					return new Promise((res, _rej) => {
						cacheInterface.getEntityPar(pid, (err, data) => {
							let par = JSON.parse(data.toString());
							if (err) {
								log.e(`Error retrieving a ${data.moduleType} from cache. Pid: ${pid}`);
								log.e(err);
								fun('Unavailable');
								return;
							}
							let impkey = par.Module + '/' + par.Entity;
							if (impkey in ImpCache) {
								BuildEnt();
								return;
							}

							GetModule(par.Module, async function (err, mod) {
								if (err) {
									log.e('Module <' + par.Module + '> not available');
									fun('Module not available');
									return;
								}

								if (!(par.Entity in mod.files)) {
									log.e('<' + par.Entity + '> not in module <' + par.Module + '>');
									fun('Null entity');
									return;
								}

								let entString = await new Promise(async (res, _rej) => {
									mod.file(par.Entity).async('string').then((string) => res(string));
								});

								log.v(`Spinning up entity ${par.Module}-${par.Entity.split('.')[0]}`);
								ImpCache[impkey] = indirectEvalImp(impkey, entString, log,
									createRequireFromModuleType(par.Module));
								BuildEnt();
							});

							function BuildEnt() {
								// TODO: rethink the whole process of having to call out a setup and start
								res(new Entity(Nxs, ImpCache[impkey], par, log));

							}
						});
					});
				});
				fun(null, EntCache[pid]);
			}

			/**
			 * Starts an instance of a module that exists in the cache.
			 * After generating, the instance Apex receives a setup and start command synchronously
			 * @param {Object} inst 		Definition of the instance to be
			 * spun up or an object of multiple definitions
			 * @param {string?} inst.Module 	The name of the module to spin up
			 * @param {Object=} inst.Par	The par of the to be encorporated with the Module Apex Par
			 * @callback fun 				(err, pid of module apex)
			 */
			async function genModule(moduleDefinition, fun = _ => _) {
				moduleDefinition = JSON.parse(JSON.stringify(moduleDefinition));
				let moduleDefinitions = moduleDefinition;
				if ('Module' in moduleDefinition && (typeof moduleDefinition.Module == 'string')) {
					moduleDefinitions = { 'Top': moduleDefinition };
				}

				let Setup = {};
				let Start = {};
				let PromiseArray = [];
				let symbols = {};

				// loop over the keys to assign pids to the local dictionary and the
				// module definitions (moduleDefinitions)
				for (let key in moduleDefinitions) {
					symbols[key] = genPid();
				}

				//compile each module
				for (let moduleKey in moduleDefinitions) {
					//do a GetModule and compile instance for each
					PromiseArray.push(new Promise((res, _rej) => {
						let inst = moduleDefinitions[moduleKey];
						GetModule(inst.Module, async function (err, mod) {
							if (err) {
								log.e('GenModule err -', err);
								fun(err);
								return;
							}
							let pidapx = symbols[moduleKey];

							for (let key in inst.Par) {
								let val = inst.Par[key];

								if (typeof val == 'string') {
									if (val.startsWith('$')) {
										let symbol = val.substr(1);
										if (symbol in symbols) {
											inst.Par[key] = symbols[symbol];
										} else {
											log.w(`${symbol} not in Module key list`);
											log.v(`${Object.keys(symbols)}`);
										}
									}

									if (val.startsWith('\\')) {
										let escaping = val.charAt(1);
										if (escaping == '$' || escaping == '\\') {
											//these are valid escape character
											inst.Par[key] = val.substr(1);
										} else {
											//invalid
											log.w(`\\${escaping} is not a valid escape sequence, ignoring.`);
										}
									}
								} else {
									inst.Par[key] = val;
								}
							}

							await compileInstance(pidapx, inst);

							let schema = await new Promise(async (res, _rej) => {
								if ('schema.json' in mod.files) {
									mod.file('schema.json').async('string').then(function (schemaString) {
										res(JSON.parse(schemaString));
									});
								} else {
									log.e('Module <' + inst.Module + '> schema not in ModCache');
									res();
									return;
								}
							});

							if ('$Setup' in schema.Apex)
								Setup[pidapx] = schema.Apex['$Setup'];
							if ('$Start' in schema.Apex)
								Start[pidapx] = schema.Apex['$Start'];
							res();
						});
					}));
				}

				await Promise.all(PromiseArray);

				log.v('Modules', JSON.stringify(symbols, null, 2));
				log.v('Setup', JSON.stringify(Setup, null, 2));
				log.v('Start', JSON.stringify(Start, null, 2));

				await setup();

				await start();

				fun(null, ('Top' in symbols) ? symbols['Top'] : null, symbols);

				/**
			 * Call setup on the required Module Apexes
			 */
				async function setup() {
					//build the setup promise array
					let setupArray = [];

					for (let pid in Setup) {
						setupArray.push(new Promise((resolve, _reject) => {
							let com = {};
							com.Cmd = Setup[pid];
							com.Passport = {};
							com.Passport.To = pid;
							com.Passport.Pid = genPid();
							sendMessage(com, resolve);
						}));
					}

					await Promise.all(setupArray);
				}

				/**
				 * Call Start on the required Module Apexes
				 */
				async function start() {
					//build the setup promise array
					let startArray = [];

					for (let pid in Start) {
						startArray.push(new Promise((resolve, _reject) => {
							let com = {};
							com.Cmd = Start[pid];
							com.Passport = {};
							com.Passport.To = pid;
							com.Passport.Pid = genPid();
							sendMessage(com, resolve);
						}));
					}

					await Promise.all(startArray);
				}
			}

			/**
			 * Generate array of entities from module
			 * Module must be in cache
			 *
			 * @param {string} pidapx 		The first parameter is the pid assigned to the Apex
			 * @param {object} inst
			 * @param {string} inst.Module	The module definition in dot notation
			 * @param {object} inst.Par		The par object that defines the par of the instance
			 * @param {boolean} saveRoot	Add the setup and start functions of the
			 * apex to the Root.Setup and start
			 */
			async function compileInstance(pidapx, inst, saveRoot = false) {
				log.v('compileInstance', pidapx, JSON.stringify(inst, null, 2));
				let Local = {};
				let modnam = (typeof inst.Module == 'object') ? inst.Module.Module : inst.Module;
				let mod;
				let ents = [];
				modnam = modnam.replace(/:\//g, '.');

				if (modnam in ModCache) {
					mod = ModCache[modnam];
				} else {
					log.e('Module <' + modnam + '> not in ModCache');
					process.exit(1);
					return;
				}

				let schema = await new Promise(async (res, rej) => {
					if ('schema.json' in mod.files) {
						mod.file('schema.json').async('string').then(function (schemaString) {
							res(JSON.parse(schemaString));
						});
					} else {
						log.e('Module <' + modnam + '> schema not in ModCache');
						process.exit(1);
						rej();
						return;
					}
				});

				let entkeys = Object.keys(schema);

				//set Pids for each entity in the schema
				for (let j = 0; j < entkeys.length; j++) {
					let entkey = entkeys[j];
					if (entkey === 'Apex') {
						Local[entkey] = pidapx;
					} else {
						Local[entkey] = genPid();
					}
				}

				//unpack the par of each ent
				for (let j = 0; j < entkeys.length; j++) {
					let entkey = entkeys[j];
					//start with the pars from the schema
					let ent = schema[entkey];
					ent.Pid = Local[entkey];
					ent.Module = modnam;
					ent.Apex = pidapx;

					//unpack the inst pars to the par of the apex of the instance
					if (entkey == 'Apex' && 'Par' in inst) {
						let pars = Object.keys(inst.Par);
						for (let ipar = 0; ipar < pars.length; ipar++) {
							let par = pars[ipar];
							ent[par] = inst.Par[par];
						}
					}

					//pars all values for symbols
					let pars = Object.keys(ent);
					for (let ipar = 0; ipar < pars.length; ipar++) {
						let par = pars[ipar];
						let val = ent[par];
						if (entkey == 'Apex' && saveRoot) {
							// if (par == '$Setup') { Setup[ent.Pid] = val; }
							// if (par == '$Start') { Start[ent.Pid] = val; }
						}
						ent[par] = await symbol(val);
					}
					ents.push(ent);
				}

				let entsPromise = [];

				for (let par of ents) {
					entsPromise.push((async function () {
						let impkey = modnam + '/'+ par.Entity;
						if (!(impkey in ImpCache)) {
							let entString = await new Promise(async (res, _rej) => {
								if (!(par.Entity in mod.files)){
									log.e('Entity <'+par.Entity+'> not in Module <' + modnam + '>');
									process.exit(1);
									return;
								}
								mod.file(par.Entity).async('string')
									.then((string) => res(string));
							});
							ImpCache[impkey] = indirectEvalImp(impkey, entString, log,
								createRequireFromModuleType(modnam));
						}
						EntCache[par.Pid] = new Volatile(new Entity(Nxs, ImpCache[impkey], par, log));
						cacheInterface.EntIndex[par.Pid] = par.Apex;
					})());
				}
				await Promise.all(entsPromise);

				async function symbol(val) {
					if (typeof val === 'object') {
						if (Array.isArray(val)) {
							val.map(v => symbol(v));
							val = await Promise.all(val);
						} else {
							for (let key in val) {
								val[key] = await symbol(val[key]);
							}
						}
						return val;
					}
					if (typeof val !== 'string')
						return val;
					let sym = val.substr(1);
					if (val.charAt(0) === '#' && sym in Local)
						return Local[sym];
					if (val.charAt(0) === '\\')
						return sym;
					return val;
				}
			}

			/**
			 * For retrieving modules
			 * Modules come from the cache directory on the harddrive or the
			 * ModCache if its already been read to RAM.
			 * @param {Object} modRequest
			 * @param {String} modRequest.Module
			 * @param {String=} modRequest.Source
			 * @param {Function} fun
			 * @returns mod
			 */
			function GetModule(ModName, fun = _ => _) {
				ModName = ModName.replace(/:\//g, '.');
				if (ModName in ModCache) return fun(null, ModCache[ModName]);
				else cacheInterface.getModule(ModName, (err, moduleZip) => {
					if (err) return fun(err);
					ModCache[ModName] = moduleZip;
					return fun(null, ModCache[ModName]);
				});
			}

		})(this.__options);
	};
};

if (!module.parent) module.exports(process.argv);


/**
 * Module dependencies.
 */
var util = require('util')
    , OAuth2Strategy = require('passport-oauth').OAuth2Strategy
    , InternalOAuthError = require('passport-oauth').InternalOAuthError;

/**
 * `Strategy` constructor.
 *
 * The Dropbox authentication strategy authenticates requests by delegating to
 * Dropbox using the OAuth 2.0 protocol.
 *
 * Applications must supply a `verify` callback which accepts an `accessToken`,
 * `refreshToken` and service-specific `profile`, and then calls the `done`
 * callback supplying a `user`, which should be set to `false` if the
 * credentials are not valid.  If an exception occurred, `err` should be set.
 *
 * Options:
 *   - `apiVersion`    (optional) the Dropbox API version to use (either '1' or '2'). Default is '1'.
 *   - `clientID`      your Dropbox application's app key found in the App Console
 *   - `clientSecret`  your Dropbox application's app secret
 *   - `callbackURL`   URL to which Dropbox will redirect the user after granting authorization
 *
 * Examples:
 *
 *     passport.use(new DropboxStrategy({
 *         clientID: 'yourAppKey',
 *         clientSecret: 'yourAppSecret'
 *         callbackURL: 'https://www.example.net/auth/dropbox-oauth2/callback',
 *       },
 *       function(accessToken, refreshToken, profile, done) {
 *         User.findOrCreate(..., function (err, user) {
 *           done(err, user);
 *         });
 *       }
 *     ));
 *
 * @param {Object} options
 * @param {Function} verify
 * @api public
 */
function Strategy(options, verify) {
  var supportedApiVersions = ['1', '2'],
      defaultOptionsByApiVersion = {
        1: {
          authorizationURL: 'https://www.dropbox.com/1/oauth2/authorize',
          tokenURL: 'https://api.dropbox.com/1/oauth2/token',
          scopeSeparator: ',',
          customHeaders: {}
        },
        2: {
          authorizationURL: 'https://www.dropbox.com/oauth2/authorize',
          tokenURL: 'https://api.dropbox.com/oauth2/token',
          scopeSeparator: ',',
          customHeaders: {
            'Content-Type': 'application/json'
          }
        }
      };

  options = options || {};

  if (options.apiVersion != null && supportedApiVersions.indexOf(options.apiVersion.toString()) === -1) {
    throw new Error('Unsupported Dropbox API version. Supported versions are "1" and "2".');
  }

  this._apiVersion = options.apiVersion || '1';

  options.authorizationURL = options.authorizationURL || defaultOptionsByApiVersion[this._apiVersion].authorizationURL;
  options.tokenURL = options.tokenURL || defaultOptionsByApiVersion[this._apiVersion].tokenURL;

  options.scopeSeparator = options.scopeSeparator || defaultOptionsByApiVersion[this._apiVersion].scopeSeparator;
  options.customHeaders = options.customHeaders || defaultOptionsByApiVersion[this._apiVersion].customHeaders;

  OAuth2Strategy.call(this, options, verify);
  this.name = 'dropbox-oauth2';
}

/**
 * Inherit from `OAuth2Strategy`.
 */
util.inherits(Strategy, OAuth2Strategy);

/**
 * Use a different method of the OAuth2Strategy for making an external request to the selected Dropbox API version.
 * Currently API v2 supports only POST requests for retrieving the user's profile.
 *
 * @param {String} accessToken
 * @param {Function} callback
 * @private
 */
Strategy.prototype._retrieveUserProfile = function(accessToken, callback) {
  if (this._apiVersion === '1') {
    this._oauth2.get('https://api.dropbox.com/1/account/info', accessToken, callback);
  }
  else if (this._apiVersion === '2') {
    // we have to provide the string 'null' as the JSON body otherwise Dropbox will complain about it not being valid.
    this._oauth2._request('POST', 'https://api.dropboxapi.com/2/users/get_current_account',
        {'Authorization': this._oauth2.buildAuthHeader(accessToken) }, 'null', accessToken, callback);
  }
};

/**
 * Retrieve user profile from Dropbox.
 *
 * This function constructs a normalized profile, with the following properties:
 *
 *   - `provider`         always set to `dropbox`
 *   - `id`               the user's unique Dropbox ID
 *   - `displayName`      a name that can be used directly to represent the name of a user's Dropbox account
 *   - `emails`           the user's email address
 *
 * @param {String} accessToken
 * @param {Function} done
 * @api protected
 */
Strategy.prototype.userProfile = function(accessToken, done) {
  this._retrieveUserProfile(accessToken, function (err, body, res) {
    if (err) { return done(new InternalOAuthError('failed to fetch user profile', err)); }

    try {
      var json = JSON.parse(body);

      var profile = { provider: 'dropbox' };

      if (this._apiVersion === '1') {
        profile.id = json.uid;
        profile.displayName = json.display_name;
        profile.name = {
          familyName: json.name_details.surname,
          givenName: json.name_details.given_name,
          middleName: ''
        };
        profile.emails = [{ value: json.email }];
      }
      else if (this._apiVersion === '2') {
        profile.id = json.account_id;
        profile.displayName = json.name.display_name;
        profile.name = {
          familyName: json.name.surname,
          givenName: json.name.given_name,
          middleName: ''
        };
        profile.emails = [{ value: json.email }];
      }

      profile._raw = body;
      profile._json = json;

      done(null, profile);
    } catch(e) {
      done(e);
    }
  }.bind(this));
};

/**
 * Expose `Strategy`.
 */
module.exports = Strategy;


module.exports = genesis;
function genesis(__options = {}) {
	// imports

	const createLogger = require('./Logger.js');
	const log = checkFlag('logger') ? __options.logger : createLogger(__options);
	const fs = require('fs');
	const Path = require('path');
	const endOfLine = require('os').EOL;
	const proc = require('child_process');
	const jszip = require('jszip');
	const CacheInterface = require('./Cache.js');
	const { Broker } = require('./broker.js');
	const BrokerCache = {};
	let cacheInterface;

	function checkFlag(flag) {
		// console.dir(__options);
		return flag in __options && __options[flag];
	}

	return new Promise(async (resolveMain, rejectMain) => {
		// log.time('genesis');
		// Genesis globals
		let Uuid; //Uuid npm package (v4.js)
		let Params = __options; 					// The set of Macros for defining paths
		let CWD = __options.cwd; 					// The current working directory
		let CacheDir = __options.cache; 	// The location of where the Cache will be stored

		log.i(`Initializing the Compile Engine in ${__options.state} Mode`);
		let compileTimer = log.time('Compile');

		try {
			log.i(' [Process Config]'.padStart(80, '='));
			log.v(`State: ${__options.state} mode`);
			log.i('Genesis Starting');
			log.v(`CWD set to ${CWD}`);
			log.i('Loading the system configuration file ...');
			// conifg, or path to it, is in Params.Config. Defaults to "config.json" in CWD
			let tempConfig = undefined;

			if (typeof Params.config == 'object') tempConfig = Params.config;
			else {
				Params.config = Path.resolve(Params.config || Path.join(CWD, 'config.json'));
				if (!(fs.existsSync(Params.config))) {
					rejectMain('Specified configuration file does not exist ' + Params.config);
					return;
				}

				try {
					// Read in the main config and make sure it's a valid .json file
					tempConfig = JSON.parse(fs.readFileSync(Params.config));
				} catch (e) {
					rejectMain('Specified configuration file is in an unparsable format. ' + Params.config);
					return;
				}
			}

			const setupTimer = log.time('Setup');
			let systemTemplate = await setup(tempConfig, true);
			log.timeEnd(setupTimer);

			const genesisTimer = log.time('Genesis');
			await genesis(systemTemplate);
			log.timeEnd(genesisTimer);

		} catch (e) {
			rejectMain(e);
			return;
		}

		/**
		 * Bulds the system
		 */
		function setup(tempConfig, root = true) {
			let Config = {}; 				// The parsed system configuration in JSON format
			let Apex = {}; 					// {<Name>: <pid of Apex>}
			let Modules = {};				// {<Name>: <mod desc>}
			let ModCache = {}; 			// {<folder>: <module>}

			return new Promise(async (resolveSetup, rejectSetup) => {

				//stores the processed config in the Config object inside of setup
				let timer = log.time('processSources');
				processSources(tempConfig);
				log.timeEnd(timer);

				// Print out the parsed config
				log.v(`Pre-Processed config: \n${JSON.stringify(Config, null, 2)}`);

				log.i('Retrieving modules ...');

				timer = log.time('generateModuleCatalog');
				generateModuleCatalog();
				log.timeEnd(timer);

				log.v(`Module List:\n\t${Object.keys(Modules).join('\n\t')}`);

				timer = log.time('retrieveModules');
				await retrieveModules(Modules);
				log.timeEnd(timer);

				log.i('Processing configuration links and dependencies ...');

				timer = log.time('buildApexInstances');
				await buildApexInstances(root);
				log.timeEnd(timer);

				log.v(`Processed config: \n${JSON.stringify(Config, null, 2)}`);

				resolveSetup({ Config, Apex, ModCache });

				return;

				//////////////////////////////////////////////////////////////
				//
				//	Only helper functions defined below in this scope
				//

				async function retrieveModules(modules) {
					modules = JSON.parse(JSON.stringify(modules));
					const xgrls = [];
					for (const moduleName in modules) {
						const xgrl = Config.Sources[modules[moduleName].Source];
						if (xgrls.indexOf(xgrl) === -1) xgrls.push(xgrl);
						modules[moduleName].Source = xgrl
					}
					// console.dir(xgrls);

					let promises = [];

					for (const xgrl of xgrls) {
						promises.push(new Promise(async (res) => {
							let broker;
							if (xgrl in BrokerCache) {
								broker = BrokerCache[xgrl];
							} else {
								const timer = log.time('Booting Broker');
								broker = new Broker(xgrl, {
									...__options
								});
								BrokerCache[xgrl] = broker;
								await broker.startup;
								log.timeEnd(timer);
							}

							const modulePromises = [];
							for (const moduleName in modules) {
								if (modules[moduleName].Source === xgrl) {
									// console.log(`${moduleName} => ${xgrl}`);
									modulePromises.push(new Promise(async (res) => {
										ModCache[moduleName] = await broker.getModule({
											Module: moduleName,
											Version: modules[moduleName].Version || undefined
										});
										res();
									}))
								}
							}
							await Promise.all(modulePromises);
							res();
						}))
					}

					await Promise.all(promises);

					return;
				}

				/**
				 * Reads in the given config and fills in the Sources Macros
				 */
				function processSources(cfg) {

					if (typeof cfg['Sources'] === 'undefined') {
						log.e('You must defined a Sources object.\n');
						rejectSetup('You must defined a Sources object.');
						return;
					}
					let val, sources, subval;
					for (let key in cfg) {
						val = cfg[key];
						if (key == 'Sources') {
							Config.Sources = {};
							sources = cfg['Sources'];
							for (let subkey in sources) {
								subval = sources[subkey];
								switch (typeof subval) {
									case 'string': {
										Config.Sources[subkey] = Macro(subval);
										break;
									}
									case 'object': {
										Config.Sources[subkey] = {};
										for (let id in subval) {
											Config.Sources[subkey][id.toLowerCase()] =
												(typeof subval[id] == 'string') ?
													Macro(subval[id]) : subval[id];
										}
										if (!('port' in Config.Sources[subkey])) {
											Config.Sources[subkey]['port'] = 27000;
										}
										break;
									}
									default: {
										log.e(`Invalid Source ${subkey} of type ${typeof subval}.` +
											'Must be of type string or object');
									}
								}
							}
						} else {
							Config[key] = val;
						}
					}
				}

				/**
				 * Create a list of all required modules and their brokers
				 */
				function generateModuleCatalog() {
					// Create new cache and install high level
					// module subdirectories. Each of these also
					// has a link to the source of that module (Module.json).
					let keys = Object.keys(Config.Modules);
					for (let i = 0; i < keys.length; i++) {
						let key = keys[i];
						if (key == 'Deferred') {
							let arr = Config.Modules['Deferred'];
							for (let idx = 0; idx < arr.length; idx++) {
								let mod = arr[idx];
								log.v(`Deferring ${mod.Module || mod}`);
								if (typeof mod == 'string') {
									log.w('Adding Module names directly to Deferred is deprecated');
									log.w(`Deferring { Module: '${mod}' } instead`);
									mod = { Module: mod };
								}
								if (!('Module' in mod)) {
									log.e('Malformed Deferred Module listing', mod);
									rejectSetup('Malformed Deferred Module listing');
									return;
								}
								logModule(key, mod);
							}
						} else {
							if (typeof Config.Modules[key].Module != 'string') {
								log.e('Malformed Module Definition');
								log.e(JSON.stringify(Config.Modules[key], null, 2));
							}
							logModule(key, Config.Modules[key]);
						}
					}

					/**
					 * Add the module to the Modules object if unique
					 * @param {object} mod 		The module object
					 * @param {string} mod.Module	The name of the module
					 * @param {object, string} mod.Source The Module broker or path reference
					 */
					function logModule(key, mod) {
						let folder = mod.Module.replace(/[/:]/g, '.');

						if (!('Source' in mod)) {
							log.e(`No Source Declared in module: ${key}: ${mod.Module}`);
							rejectSetup(`No Source Declared in module: ${key}`);
							return;
						}

						let source = {
							Source: mod.Source,
							Version: mod.Version
						};

						if (!(folder in Modules)) {
							Modules[folder] = source;
						} else {
							if (Modules[folder].Source != source.Source
								|| (Modules[folder].Version != source.Version)) {
								log.e(`Broker Mismatch Exception: ${key}\n`
									+ `${JSON.stringify(Modules[folder], null, 2)} - `
									+ `\n${JSON.stringify(source, null, 2)}`);
								rejectSetup('Broker Mismatch Exception');
								return;
							}
						}
					}
				}

				/**
				 * Write the modules and all instances to the cache
				 */
				async function buildApexInstances(processPidReferences) {
					if (processPidReferences) {
						// Assign pids to all instance in Config.Modules
						for (let instname in Config.Modules) {
							if (instname == 'Deferred')
								continue;
							Apex[instname] = genPid();
						}
						log.v('Apex List', JSON.stringify(Apex, null, 2));
					}

					// Now populate all of the modules from config.json
					for (let instname in Config.Modules) {
						if (instname === 'Deferred')
							continue;
						await processApexPar(Apex[instname], Config.Modules[instname], processPidReferences);
					}
				}

				/**
				 * ----------------------------------------------------CompileModule
				 * @param {String} apx 						The Pid of the Apex
				 * @param {Object} inst 					the par of the Apex
				 * @param {Boolean} processPidReferences 	Process the $ references or not
				 */
				async function processApexPar(apx, inst, processPidReferences) {
					inst = symbolPhase0(inst);
					if (processPidReferences) inst.Par = await symbolPhase1(inst.Par);
					inst.Par = await symbolPhase2(inst.Par);
					inst.Par = await symbolPhase3(inst.Par);
					return;

					//process {} references
					function symbolPhase0(obj) {
						for (let key in obj) {
							if (typeof obj[key] == 'string') obj[key] = Macro(obj[key]);
							else if (typeof obj[key] == 'object') obj[key] = symbolPhase0(obj[key]);
						}
						return obj;
					}

					//process $ references
					async function symbolPhase1(val) {
						if (typeof val === 'object') {
							if (Array.isArray(val)) {
								val = await Promise.all(val.map(v => symbolPhase1(v)));
							} else {
								for (let key in val) {
									val[key] = await symbolPhase1(val[key]);
								}
							}
							return val;
						}

						if (typeof val !== 'string')
							return val;
						let sym = val.substr(1);
						if (val.charAt(0) === '$') {
							if (sym in Apex) return Apex[sym];
							else {
								log.v(sym, Apex);
								log.e(`Symbol ${val} is not defined`);
								rejectSetup(`Symbol ${val} is not defined`);
								return;
							}
						}
						return val;
					}

					//process @ system directives
					async function symbolPhase2(val) {
						if (typeof val === 'object') {
							if (Array.isArray(val)) {
								val = await Promise.all(val.map(v => symbolPhase2(v)));
							} else {
								for (let key in val) {
									val[key] = await symbolPhase2(val[key]);
								}
							}
							return val;
						}

						if (typeof val !== 'string' || (!val.startsWith('@')))
							return val;
						let [directive, path] = val.split(':').map(v => v.toLocaleLowerCase().trim());

						if (directive == '@system') {
							let directiveTimer = log.time(val);
							let stepTimer = log.time('fs');
							let systemPath = Params.config ? Path.dirname(Params.config) : CWD;

							if (!(Path.isAbsolute(path))) {
								path = Path.join(Path.resolve(systemPath), path);
							}

							let tempConfig;

							if (!fs.existsSync(path)) {
								rejectSetup(`Specified configuration file does not exist: ${path}`);
								return;
							}
							try {
								tempConfig = JSON.parse(fs.readFileSync(path));
							} catch (e) {
								rejectSetup('Specified configuration file is in an unparsable format.');
								return;
							}
							log.timeEnd(stepTimer);

							//TODO parse out all $'s, replace with \\$
							let systemObject;
							try {
								systemObject = await setup(tempConfig, false);
							} catch (e) {
								rejectSetup(`Error processing subsystem at: ${path}`);
								return;
							}

							try { fs.mkdirSync(Path.join(CWD, 'Static')); } catch (e) {
								log.v(e);
							}

							stepTimer = log.time('bower');
							await new Promise(async resolve => {
								// let zip = new jszip();
								// let cacheBuffer = Buffer.from(systemObject.Cache, 'base64');
								// zip.loadAsync(cacheBuffer).then(async (a) => {
								// 	for (let key in a.files) {
								// 		if (key === 'manifest.json') continue;
								for (let moduleType in systemObject.ModCache) {
									let modZip = new jszip();
									// let moduleZipBinary = await zip.file(key).async('base64');
									modZip = await new Promise((res) => {
										let modZipBuffer = Buffer.from(systemObject.ModCache[moduleType], 'base64');
										modZip.loadAsync(modZipBuffer).then(zip => {
											res(zip);
										});
									});
									if (!('bower.json' in modZip.files)) continue;

									let bowerjson = await modZip.file('bower.json').async('string');
									let dependencies = JSON.parse(bowerjson).dependencies;
									let packageArray = [];
									for (let bowerModuleName in dependencies) {
										if (dependencies[bowerModuleName].indexOf('/') > 0) {
											packageArray.push(`${dependencies[bowerModuleName]}`);
										}
										else {
											packageArray.push(`${bowerModuleName}#`
												+ `${dependencies[bowerModuleName]}`);
										}
										await new Promise(res => {
											proc.execSync('bower install "--config.directory='
												+ `${Path.join(CWD, 'Static',
													'bower_components')}" "${packageArray.join('" "')}"`);
											log.v(`[BOWER] Installed ${packageArray.join(', ')}`);
											res();
										});
									}
									let bowerComponentsDir = Path.join(CWD, 'Static',
										'bower_components');
									proc.execSync('bower install "--config.directory='
										+ `${bowerComponentsDir}" "${packageArray.join('" "')}"`);
									log.v(`[BOWER] Installed ${packageArray.join(', ')}`);
								}
								resolve();
							});
							log.timeEnd(stepTimer);

							stepTimer = log.time('zip it');
							//zip up the ModCache for export to the browser or other subsystems
							let zip = await new Promise(resolveZip => {
								let zip = new jszip();
								let man = [];
								for (let folder in systemObject.ModCache) {
									let mod = Buffer.from(systemObject.ModCache[folder], 'base64');
									man.push(folder);
									zip.file(folder, mod, {
										date: new Date('April 2, 2010 00:00:01')
										//the date is required for zip consistency
									});
								}
								zip.file('manifest.json', JSON.stringify(man), {
									date: new Date('April 2, 2010 00:00:01')
									//the date is required for zip consistency
								});
								zip.generateAsync({ type: 'base64' }).then(function (data) {
									resolveZip({
										'Config': systemObject.Config,
										'Cache': data
									});
								});
							});
							log.timeEnd(stepTimer);
							log.timeEnd(directiveTimer);

							return zip;

						}
						return val;
					}

					//process @ files and directories
					async function symbolPhase3(val) {
						if (typeof val === 'object') {
							if (Array.isArray(val)) {
								val = await Promise.all(val.map(v => symbolPhase3(v)));
							} else {
								for (let key in val) {
									val[key] = await symbolPhase3(val[key]);
								}
							}
							return val;
						}
						if (typeof val !== 'string' || (!val.startsWith('@')))
							return val;
						if (val.charAt(0) === '@') {
							let directive = val.substr(0);
							val = val.split(':');
							let key = val[0].toLocaleLowerCase().trim();
							let encoding = undefined;
							if (key.split(',').length == 2) {
								key = key.split(',')[0].trim();
								let _encoding = key.split(',')[1].trim();
							}
							val = val.slice(1).join(':').trim();
							let directiveTimer = log.time(directive);
							switch (key) {
								case '@filename':
								case '@file': {
									log.v(`Compiling ${directive}`);
									let path;
									try {
										let systemPath = Params.config ? Path.dirname(Params.config) : CWD;
										if (Path.isAbsolute(val))
											path = val;
										else {
											path = Path.join(Path.resolve(systemPath), val);
										}
										log.timeEnd(directiveTimer);
										return fs.readFileSync(path).toString(encoding);
									} catch (err) {
										log.e('@file: (compileInstance) Error reading file ', path);
										log.w(`Module ${inst.Module} may not operate as expected.`);
									}
									break;
								}
								case '@folder':
								case '@directory': {
									log.v(`Compiling ${directive}`);
									let dir;
									try {
										let systemPath = Params.config ? Path.dirname(Params.config) : CWD;
										if (Path.isAbsolute(val))
											dir = val;
										else
											dir = Path.join(Path.resolve(systemPath), val);
											let _return = await buildDir(dir);
											log.timeEnd(directiveTimer);
										return _return;
									} catch (err) {
										og.e('Error reading directory ', dir);
										og.w(`Module ${inst.Module} may not operate as expected.`);
									}
									break;
								}
								default: {
									log.w(`Key ${key} not defined.`
										+ `Module ${inst.Module} may not operate as expected.`);
								}
							}
							log.timeEnd(directiveTimer);
						}
						return val;
					}
				}

				/**
				 * Build an object to represent a directory
				 * @param {String} path
				 */
				async function buildDir(path) {
					let dirObj = {};
					if (fs.existsSync(path)) {
						let files = fs.readdirSync(path);
						let itemPromises = [];
						for (let file of files) {
							itemPromises.push(new Promise(async (resolve) => {
								let curPath = path + '/' + file;
								if (fs.lstatSync(curPath).isDirectory()) {
									// recurse
									dirObj[file] = await buildDir(curPath);
									resolve();
								} else {
									fs.readFile(curPath, function (err, data) {
										// log.v(curPath.length > 80 ? curPath.substr(0, 35)
										// + ' ... ' + curPath.substr(-40, 40) : curPath);
										dirObj[file] = data.toString();
										resolve();
									});
									// dirObj[file] = fs.readFileSync(curPath).toString(encoding);
								}
							}));
						}
						await Promise.all(itemPromises);
						return dirObj;
					}
				}

				/**
				 * generate a 32 character hexidecimal pid
				 */
				function genPid() {
					if (!Uuid) {
						// module.paths = [Path.join(Path.resolve(CacheDir), 'node_modules')];
						Uuid = require('uuid/v4');
					}
					let str = Uuid();
					let pid = str.replace(/-/g, '').toUpperCase();
					return pid;
				}

				/**
				 * replace the {} macros passed throught the xgraph cli or __options object
				 * @param {string} str the string to look up
				*/
				function Macro(str) {
					str = str.substr(0); // copy
					for (let option in __options) {
						str = str.replace(`{${option}}`, __options[option]);
					}
					return str;
				}
			});
		}

		/**
		 * Writes the system to the cache
		 */
		async function genesis(system) {
			log.i(' [Save Cache]'.padStart(80, '='));
			log.i('Genesis Compile Start:');

			let cacheState = null;
			if (fs.existsSync(CacheDir)) cacheState = 'exists';

			cacheInterface = new CacheInterface({
				path: CacheDir, log
			});

			cleanCache();

			log.i('Saving modules and updating dependencies ...');
			await cacheModules(system.ModCache);

			if (!(__options.state == 'updateOnly')) {
				log.i('Saving entities ...');
				await cacheApexes(system.Apex, system.Config.Modules);
			}

			Stop();

			/////////////////////////////////////////////////////////////
			//
			//	Only helper functions beyond this point of this scope
			//

			/**
			*  Remove the cache if it currently exists in the given directory
			*/
			function cleanCache() {
				// Remove the provided cache directory
				if (__options.state == 'development' && cacheState) {
					__options.state = 'updateOnly';
					return;
				}
				log.v('Removing the old cache.');
				cacheInterface.clean();
			}

			/**
			 * Write the modules to the cache
			 * @param {Object} ModCache 	//the set of module zips required for this system
			 */
			async function cacheModules(ModCache) {
				let timer = log.time('cacheModules');
				let ModulePromiseArray = [];
				for (let folder in ModCache) {
					ModulePromiseArray.push(new Promise(async (res) => {
						await cacheInterface.addModule(folder, ModCache[folder]);
						log.v(`Finished installing dependencies for ${folder}`);
						res();
					}));

				}
				await Promise.all(ModulePromiseArray);

				log.timeEnd(timer);
			}

			/**
			 * Write the module Apexes to the cache
			 * @param {Object} Apexes 						//The id:Pid of each apex
			 * @param {Object} ModuleDefinitions 	//the id:ModuleDefinition from Config
			 */
			async function cacheApexes(Apexes, ModuleDefinitions) {
				let ModulePromiseArray = [];
				for (let moduleId in Apexes) {
					ModulePromiseArray.push(
						await cacheInterface.createInstance(ModuleDefinitions[moduleId], Apexes[moduleId])
					);
				}
				await Promise.all(ModulePromiseArray);
			}

			/**
			 * Resolves the main promise created during genesis call
			 */
			async function Stop() {
				log.i(`Genesis Compile Stop: ${new Date().toString()}`);
				log.i(' [Finished]'.padStart(80, '='));
				for(const xgrl in BrokerCache) {
					const broker = BrokerCache[xgrl];
					broker.cleanup();
				}
				log.timeEnd(compileTimer);
				resolveMain();
			}
		}
	});
}


const log = new (require('signale').Signale)({
	disabled: false,
	interactive: false,
	stream: process.stdout,
	types: {
		v: {
			badge: '',
			color: 'grey',
			label: 'verbose'
		},
		d: {
			badge: '',
			color: 'magenta',
			label: 'debug'
		},
		i: {
			badge: '',
			color: 'cyan',
			label: 'info'
		},
		w: {
			badge: '',
			color: 'yellow',
			label: 'warning'
		},
		e: {
			badge: '',
			color: 'red',
			label: 'error'
		}
	}
});
const fs = require('fs');
const path = require('path');
const https = require('https');
const appdata = path.join((process.env.APPDATA || path.join(process.env.HOME,
	(process.platform == 'darwin' ? 'Library/Preferences' : ''))), '.xgraph');
try { fs.mkdirSync(appdata); } catch (e) { ''; }

module.exports = {
	getProtocolObject
};

async function getProtocolObject(xgrl) {
	let protocol = xgrl.split(/:\/\//)[0];
	let _domain = xgrl.split(/:\/\//)[1];

	if (protocol.split(/(\\|\/)/).length>1){
		_domain = protocol;
		protocol = 'fs';
	}

	let protocolObject = await getProtocolModule(protocol);

	//need to rebuild the par from the argsArr

	// "exmaple.com:23897"
	let argsArr = _domain.split(/:/);

	//replace with given arguments
	for (let i = 0; i < argsArr.length; i++) {
		for (let val in protocolObject.Par) {
			if (protocolObject.Par[val] == `%${i+1}`) protocolObject.Par[val] = argsArr[i];
		}
	}
	//replace with given defaults
	for (let def in protocolObject.Default) {
		for (let val in protocolObject.Par) {
			if (protocolObject.Par[val] == def) protocolObject.Par[val] = protocolObject.Default[def];
		}
	}

	let moduleDefinition = {
		Module: 'BrokerProxy',
		Par: protocolObject.Par
	};

	const _ = {
		moduleDefinition,
		zip: protocolObject.Module,
		config: ('Config' in protocolObject)? protocolObject.Config : undefined
	};

	// console.dir(_);

	return _;
}

/**
* load protocol to access modules
*/
function getProtocolModule(protocol) {
	return new Promise(function (resolve, reject) {
		let cacheFilepath = path.join(appdata, protocol);
		if (fs.existsSync(cacheFilepath)) {
			return resolve(JSON.parse(fs.readFileSync(cacheFilepath).toString()));
		}

		let options = {
			host: 'protocols.xgraphdev.com',
			port: 443,
			path: '/' + protocol,
			method: 'GET',
			rejectUnauthorized: false,
		};

		let req = https.request(options, function (res) {
			res.setEncoding('utf8');
			let response = '';
			res.on('data', function (chunk) {
				response += chunk;
			});
			res.on('end', _ => {
				try {
					resolve(JSON.parse(response));
					try { fs.writeFileSync(cacheFilepath, response); } catch (e) {
						reject({
							code: 1,
							text: `fail to save protocol at ${cacheFilepath}` +
								'\n delete file and try again'
						});
					}
				} catch (e) {
					reject({ code: 0, text: 'try and retrieve locally' });
				}
			});
		});

		req.on('error', function (e) {
			log.e('problem with request: ' + e.message);
			reject({ code: 1, text: 'problem with request: ' + e.message });
		});

		// write data to request body
		req.end();
	});
}


const Path = require('path');
const SemVer = require('./SemVer.js');
// const fs = require('fs');
const jszip = require('jszip');
const ver130 = new SemVer('1.3');
const dotCache = { version: '1.3.0' };
const Uuid = require('uuid/v4');
// const { spawn } = require('child_process');
//This node module provides all the interface capabilities to an xgraph cache directory.

module.exports = class CacheInterface {
	/**
	 *Creates an instance of CacheInterface.
	 * @param {object} __options
	 * @param {string} __options.path the root of cache. this folder will be the one with a .cache inside it.
	 * @param {string=} __options.log logger object, needs methods: d, v, i, w, and e
	 */
	constructor(__options) {
		this.__options = __options;
		let backupLog = {
			d: _ => _, v: _ => _, i: _ => _, w: _ => _, e: _ => _
		};
		this.log = {
			v: (..._) => {
				(__options.log || backupLog).v('[CACHE]', ..._);
			},
			d: (..._) => {
				(__options.log || backupLog).d('[CACHE]', ..._);
			},
			w: (..._) => {
				(__options.log || backupLog).w('[CACHE]', ..._);
			},
			e: (..._) => {
				(__options.log || backupLog).e('[CACHE]', ..._);
			},
			i: (..._) => {
				(__options.log || backupLog).i('[CACHE]', ..._);
			}
		};

		this.modCache = {};

		this.initDirectories();
	}

	get EntIndex() {
		return this._entIndex;
	}

	set EntIndex(val) {
		this._entIndex = val;
	}

	/**
	 * generate a 32 digit hex number
	 */
	genPid() {
		let str = Uuid();
		let pid = str.replace(/-/g, '').toUpperCase();
		return pid;
	}

	initDirectories() {
		let __options = this.__options;
		let log = this.log;
		let packageString = JSON.stringify({ dependencies: {} }, null, 2);

		let packagePath = Path.join(Path.resolve(__options.path), 'package.json');
		if (!fs.existsSync(packagePath))
			fs.writeFileSync(packagePath, packageString);
		let cacheManifestPath = Path.join(Path.resolve(__options.path), 'cache.json');
		if (!fs.existsSync(cacheManifestPath)) {
			fs.writeFileSync(cacheManifestPath, JSON.stringify({
				version: '1.3.0'
			}, '\t', 1));
		}
	}

	clean() {
		this.delete();
		this.initDirectories();
	}

	delete() {
		let __options = this.__options;
		if (fs.existsSync(__options.path)) {
			// log.i(__options.path);
			this.remDir(__options.path);
			// log.i(this);
			// log.i('woo');
		}
	}

	//retrieve a module from the cache
	getModule(moduleType, fun = _ => _) {
		let __options = this.__options;
		let log = this.log;
		let cachedMod;

		if (this._version < ver130) {
			cachedMod = Path.join(__options.path, moduleType, 'Module.zip');
		} else {
			cachedMod = Path.join(__options.path, 'System', moduleType, 'Module.zip');
		}

		log.v(cachedMod);
		log.v(fs.existsSync(cachedMod));

		fs.lstat(cachedMod, function (err, stat) {
			if (err) {
				log.e(`Error retreiving ${cachedMod} from cache`);
				log.e(err);
				fun(err);
			}
			if (stat) {
				if (!stat.isDirectory()) {
					fs.readFile(cachedMod, async function (err, data) {
						if (err) {
							fun(err);
							return;
						}
						fun(null, await new Promise(async (res, _rej) => {
							let zip = new jszip();
							zip.loadAsync(data).then((mod) => res(mod));
						}));
						return;
					});
				}
			} else {
				err = `Module ${cachedMod} does not exist in the cache`;
				log.e(err);
				fun(err);
				return;
			}
		});
	}

	//adds a module to the cache
	async addModule(moduleType, moduleZip) {
		return new Promise(async (resolveModule, rejectModule) => {
			let log = this.log;

			let __options = this.__options;
			let cachedMod, libdir;

			if (this._version < ver130) {
				cachedMod = Path.join(__options.path, moduleType);
				libdir = Path.join(__options.path, moduleType);
			} else {
				cachedMod = Path.join(__options.path, 'System', moduleType);
				libdir = Path.join(__options.path, 'Lib', moduleType);
			}

			try { fs.mkdirSync(cachedMod); } catch (e) { log.v(`${cachedMod} path already exists`); }
			try { fs.mkdirSync(libdir); } catch (e) { log.v(`${cachedMod} path already exists`); }

			cachedMod = Path.join(cachedMod, 'Module.zip');

			this.modCache[moduleType] = moduleZip;

			await new Promise(res => {
				fs.writeFile(cachedMod, moduleZip, (err) => {
					res(err);
				});
			});

			//install npm dependencies
			let zipObj = new jszip();
			await zipObj.loadAsync(moduleZip);

			if ('package.json' in zipObj.files) {
				let packageString;
				packageString = await zipObj.file('package.json').async('string');
				let packagePath = Path.join(libdir, 'package.json');
				log.v(`Installing dependencies for ${moduleType}`);

				await new Promise((resolve, reject) => {
					fs.writeFile(packagePath, packageString, err => {
						if (err) return reject(err);
						resolve();
					});
				});

				await new Promise((resolve, reject) => {
					fs.writeFile(packagePath, packageString, err => {
						if (err) return reject(err);
						resolve();
					});
				});

				try {
					await new Promise((resolve, reject) => {
						//call npm install on a childprocess of node
						let npmCommand = (process.platform === 'win32' ? 'npm.cmd' : 'npm');

						let npmInstallProcess = spawn(npmCommand, [
							'install'
						], {
								cwd: Path.resolve(libdir)
							});

						// npmInstallProcess.stdout.on('data', process.stdout.write);
						// npmInstallProcess.stderr.on('data', process.stderr.write);

						npmInstallProcess.stdout.on('error', e => log.v('stdout/err: ' + e));
						npmInstallProcess.stderr.on('error', e => log.v('stderr/err: ' + e));

						npmInstallProcess.on('err', function (err) {
							log.e('Failed to start child process.');
							log.e('err:' + err);
							reject(err);
						});

						npmInstallProcess.on('exit', function (code) {
							if (code == 0) {
								// log.v(`${moduleType}: dependencies installed correctly`);
							} else {
								log.e(`${moduleType}: npm process exited with code: ${code}`);
								process.exit(1);
								reject();
							}
							fs.unlinkSync(packagePath);
							resolve();
						});
					});

				} catch (e) {
					log.e(e);
					log.e(e.stack);
					rejectModule(e);
				}

				//write the compiled package.json to disk
				fs.writeFileSync(Path.join(libdir, 'package.json'), packageString);
			} else {
				log.v(`no dependencies to install for ${moduleType}`);
				// resolve();
			}
			resolveModule(moduleType);
		});
	}

	//return the json of pars related to a single entity based on it's pid
	getEntityPar(pid, fun = _ => _) {
		// let log = this.log;
		let log = this.log;
		if (!(pid in this._entIndex)) {
			log.w('returning unknown pid', pid);
			log.w(this._entIndex);
			return fun(new Error('E_UNKNOWN_PID'), { moduleType: 'Unknown' });
		}
		let apx = this._entIndex[pid];

		if (!(apx in this._apexIndex)) {
			log.w('returning unknown apex pid');
			return fun(new Error('E_UNKNOWN_APEX_PID'), { moduleType: 'Unknown' });
		}
		let moduleType = this._apexIndex[apx];
		let __options = this.__options;
		let path;
		if (this._version < ver130) {
			path = Path.join(__options.path, moduleType, apx, `${pid}.json`);
		} else {
			path = Path.join(__options.path, 'System', moduleType, apx, `${pid}.json`);
		}

		fs.readFile(path, (err, data) => {
			if (err) {
				log.w(err);
				return fun(err, { moduleType });
			} else return fun(err, data);
		});
	}

	//delete an entity from the cache
	async deleteEntity(pid, fun = _ => _) {
		let apx = this._entIndex[pid];
		let moduleType = this._apexIndex[apx];
		let __options = this.__options;
		let apxpath;

		if (this._version < ver130) {
			apxpath = Path.join(__options.path, moduleType, apx);
		} else {
			apxpath = Path.join(__options.path, 'System', moduleType, apx);
		}

		let rmList = [];
		//we first check to see if it's an apex
		//if so we will read the directory that is the instance of
		//the module and then delete all of the entity files found therein.
		if (apx == pid) {
			let files;
			try { files = fs.readdirSync(apxpath); } catch (e) {
				log.w(e);
				return fun(null, [pid]);
			}
			for (let i = 0; i < files.length; i++) {
				rmList.push(files[i].split('.')[0]);
			}
			await remDir(apxpath);
		} else {
			rmList.push(pid);
			let path = apxpath + '/' + pid + '.json';
			fs.unlinkSync(path);
		}
		return fun(null, rmList);

		/**
		 * Recursive directory deletion
 		* @param {string} path the directory to be recursively removed
		 */
		function remDir(path) {
			return (new Promise(async (resolve, _reject) => {
				if (fs.existsSync(path)) {
					let files = fs.readdirSync(path);
					let promiseArray = [];

					for (let fileIndex = 0; fileIndex < files.length; fileIndex++) {
						promiseArray.push(new Promise(async (resolve2, _reject2) => {
							let curPath = path + '/' + files[fileIndex];
							if (fs.lstatSync(curPath).isDirectory()) {
								// recurse
								await remDir(curPath);
								resolve2();
							} else {
								// delete file
								log.v('Removing Entity ', files[fileIndex].split('.')[0]);
								fs.unlinkSync(curPath);
								resolve2();
							}
						}));
					}
					//make sure all the sub files and directories have been removed;
					await Promise.all(promiseArray);
					log.v('Removing Module Directory ', path);
					fs.rmdirSync(path);
					resolve();
				} else {
					log.v('trying to remove nonexistant path ', path);
					resolve();
				}
			}));
		}
	}

	//save an entity by providing the pars json
	saveEntityPar(parObject, fun = _ => _) {
		let moduleType = this._apexIndex[parObject.Apex];
		let __options = this.__options;
		let path;

		if (this._version < ver130) {
			path = Path.join(__options.path, moduleType, parObject.Apex);
		} else {
			path = Path.join(__options.path, 'System', moduleType, parObject.Apex);
		}

		try { fs.mkdirSync(path); } catch (e) {
			if (e.code !== 'EEXIST') log.w(e);
		}

		path = Path.join(path, `${parObject.Pid}.json`);

		fs.writeFile(path, JSON.stringify(parObject, null, 2), (err) => {
			this._entIndex[parObject.Pid] = parObject.Apex;
			fun(err, parObject.Pid);
		});
	}

	//load in a cache directory and return the Apex Index, Start, Setup and Stop dictionaries.
	async loadCache() {
		let log = this.log;
		let __options = this.__options;
		let manifestPath = Path.join(__options.path, 'cache.json');
		let setup = {}, start = {}, stop = {}, apexIndex = {}, entIndex = {};
		let manifest = await new Promise(resolve => {
			fs.lstat(manifestPath, (err, stat) => {
				if (!err && stat.isFile()) {
					resolve(JSON.parse(fs.readFileSync(manifestPath)));
				} else {
					resolve({ version: '1.2.1' });
				}
			});
		});
		let version = this._version = new SemVer(manifest.version);

		let modulesDirectory;
		if (version < new SemVer('1.3')) {
			modulesDirectory = Path.join(__options.path, '');
		} else {
			modulesDirectory = Path.join(__options.path, 'System');
		}

		let folders = fs.readdirSync(modulesDirectory);

		for (let ifold = 0; ifold < folders.length; ifold++) {
			let folder = folders[ifold];
			let path = Path.join(modulesDirectory, folder, 'Module.zip');
			if (!fs.existsSync(path))
				continue;

			parseMod(folder);

		}
		function parseMod(folder) {
			let dir = Path.join(modulesDirectory, folder);
			let instancefiles = fs.readdirSync(dir);
			for (let ifile = 0; ifile < instancefiles.length; ifile++) {
				let file = instancefiles[ifile];
				//check that it's an instance of the module
				if (file.length !== 32)
					continue;

				let path = Path.join(dir, file);
				if (fs.lstatSync(path).isDirectory()) {
					apexIndex[file] = folder;
					let instJson = JSON.parse(fs.readFileSync(Path.join(path, `${file}.json`)));

					for (let filename of fs.readdirSync(path)) {
						entIndex[Path.parse(filename).name] = file;
					}

					if ('$Setup' in instJson)
						setup[file] = instJson.$Setup;
					if ('$Start' in instJson)
						start[file] = instJson.$Start;
					if ('$Stop' in instJson)
						stop[file] = instJson.$Stop;

				}
			}
		}

		log.v('EntIndex', JSON.stringify(entIndex, null, 2));
		log.v('Setup', JSON.stringify(setup, null, 2));
		log.v('Start', JSON.stringify(start, null, 2));
		log.v('Stop', JSON.stringify(stop, null, 2));

		this._entIndex = entIndex;
		this._apexIndex = apexIndex;

		return { apexIndex, setup, start, stop };
	}

	loadDependency(moduleType, moduleName) {
		let log = this.log;
		let __options = this.__options;
		let version = this._version;
		let nodeModulesPath;
		if (version < ver130) {
			nodeModulesPath = Path.join(__options.path, moduleType, 'node_modules', moduleName);
		} else {
			nodeModulesPath = Path.join(__options.path, 'Lib', moduleType, 'node_modules', moduleName);
		}
		try {
			delete require.cache[nodeModulesPath];
			delete require.cache[moduleName];
		} catch (e) { log.w(e) }
		try {
			return require(nodeModulesPath);
		} catch (e) {
			try {
				return require(moduleName);
			} catch (e) {
				log.v(fs.existsSync(nodeModulesPath));
				log.v(moduleType, moduleName);
				log.v(nodeModulesPath.split(Path.sep).join('\n> '));
				log.e(`error loading ${moduleName}`);
				log.e(e);
				process.exit(1);
			}
		}
	}

	/**
	 * Generate array of entities from module
	 * Module must be in cache
	 *
	 * @param {string} pidapx 		The first parameter is the pid assigned to the Apex
	 * @param {object} inst
	 * @param {string} inst.Module	The module definition in dot notation
	 * @param {object} inst.Par		The par object that defines the par of the instance
	 * @param {boolean} saveRoot	Add the setup and start functions to the Root.Setup and start
	 */
	async createInstance(inst, pidapx = this.genPid()) {
		let log = this.log;
		let Local = {};
		let modnam = inst.Module;
		let mod;
		let ents = [];
		modnam = modnam.replace(/[/:]/g, '.');
		let zipmod = new jszip();
		log.v('createInstance', pidapx, JSON.stringify(inst, null, 2));

		function parseMacros(obj) {
			for (let key in obj) {
				if (typeof obj[key] == 'string') {
					if (obj[key].startsWith('#') && (obj[key].trim().split('\n').length == 1)) {

						let sym = obj[key].substr(1);
						if (sym in Local) obj[key] = Local[obj[key].substr(1)];
						else {
							log.e(`Local Symbol #${sym} is not defined`);
							throw new Error(`Local Symbol #${sym} is not defined`);
						}
					}
				}
				else if (typeof obj[key] == 'object') obj[key] = parseMacros(obj[key]);
			}
			return obj;
		}

		log.v('parseMacros', pidapx, JSON.stringify(inst, null, 2));

		//check if we have the module, put it in mod (jszip object)
		if (modnam in this.modCache) {
			//TODO Dont use then, use es8 async/await
			mod = await new Promise(async (res, _rej) => {
				zipmod.loadAsync(this.modCache[modnam]).then((zip) => {
					res(zip);
				});
			});
		} else {
			log.e('Module <' + modnam + '> not in ModCache');
			throw new Error('Module <' + modnam + '> not in ModCache');
		}

		// get the schema json from the zip
		let schema = await new Promise(async (res, rej) => {
			// log.w(mod.files);
			if ('schema.json' in mod.files) {
				mod.file('schema.json').async('string').then(function (schemaString) {
					res(JSON.parse(schemaString));
				});
			} else {
				log.e('Module <' + modnam + '> schema not in ModCache');
				process.exit(1);
				rej();
				// reject();
				return;
			}
		});

		let entkeys = Object.keys(schema);
		if (!('Apex' in schema)) {
			log.v('keys in schema.json');
			log.v(Object.keys(schema).join('\r\n'));
			throw new SyntaxError('Apex key not present in schema.json.');
		}

		//set Pids for each entity in the schema
		for (let j = 0; j < entkeys.length; j++) {
			let entkey = entkeys[j];
			if (entkey === 'Apex')
				Local[entkey] = pidapx;
			else
				Local[entkey] = this.genPid();
		}

		for (let entIndex in schema) {
			// log.w(entIndex);
			let ent = schema[entIndex];
			if (typeof ent !== 'object') throw new Error('E_INVALID_SCHEMA', { Module: modnam })
			if (entIndex === 'Apex') {
				for (let key in inst.Par) {
					ent[key] = inst.Par[key];
				}
			}
			ent.Pid = Local[entIndex];
			ent.Apex = pidapx;
			ent.Module = modnam;
			ent = parseMacros(ent);
			ents.push(ent);
		}

		log.v('Entity Pars to save:', ents);

		for (let entity of ents) {

			let path = Path.join(this.__options.path, 'System', modnam, entity.Apex);
			try { fs.mkdirSync(path); } catch (e) { ''; }
			path = Path.join(path, entity.Pid + '.json');

			await new Promise(res => {
				fs.writeFile(path, JSON.stringify(entity, null, 2), _ => {
					res();
				});
			});
		}
		return ents;
	}
};


"use strict";

// ax5.ui.palette
(function () {

    var UI = ax5.ui;
    var U = ax5.util;
    var PALETTE = void 0;

    UI.addClass({
        className: "palette"
    }, function () {

        /**
         * @class ax5palette
         * @classdesc
         * @author tom@axisj.com
         * @example
         * ```js
         * ```
         */
        return function () {
            var _this = this;

            var self = this,
                cfg = void 0;

            this.instanceId = ax5.getGuid();
            this.target = null;
            this.config = {
                clickEventName: "click",
                theme: 'default',
                animateTime: 100,
                colors: {
                    preview: {
                        width: 24,
                        height: 24,
                        cellWidth: 30
                    },
                    label: {
                        width: 80
                    },
                    slider: {
                        trackHeight: 8,
                        amount: 32,
                        handleWidth: 18,
                        handleHeight: 18
                    },
                    list: [{ label: "red", value: "#ff0000" }, { label: "orange", value: "#ff9802" }, { label: "yellow", value: "#ffff00" }, { label: "green", value: "#00ff36" }, { label: "blue", value: "#0000ff" }, { label: "purple", value: "#ba00ff" },
                    //{label: "skyblue", value: "#84e4ff"},
                    //{label: "pink", value: "#ff77c4"},
                    { label: "black", value: "#000000" }, { label: "white", value: "#ffffff" }]
                },
                controls: {
                    height: 0
                },
                columnKeys: {}
            };
            this.xvar = {};
            this.colors = [];

            cfg = this.config;

            var ENM = {
                "mousedown": ax5.info.supportTouch ? "touchstart" : "mousedown",
                "mousemove": ax5.info.supportTouch ? "touchmove" : "mousemove",
                "mouseup": ax5.info.supportTouch ? "touchend" : "mouseup"
            };

            var onStateChanged = function onStateChanged(opts, that) {
                if (opts && opts.onStateChanged) {
                    opts.onStateChanged.call(that, that);
                } else if (_this.onStateChanged) {
                    _this.onStateChanged.call(that, that);
                }

                that = null;
            };

            /**
             * get mouse position
             * @param e
             * @returns {{clientX, clientY}}
             */
            var getMousePosition = function getMousePosition(e) {
                var mouseObj = void 0,
                    originalEvent = e.originalEvent ? e.originalEvent : e;
                mouseObj = 'changedTouches' in originalEvent && originalEvent.changedTouches ? originalEvent.changedTouches[0] : originalEvent;
                // clientX, Y 쓰면 스크롤에서 문제 발생
                return {
                    clientX: mouseObj.pageX,
                    clientY: mouseObj.pageY
                };
            };

            var bindHandle = function bindHandle(item) {
                item.originalTrackWidth = item.$track.width();
                item.trackWidth = item.originalTrackWidth - cfg.colors.slider.handleWidth / 5;
                var handleLeft = amountToHandleLeft(item, item._amount);

                // handleLeft 가 범위를 벗어나면?
                if (handleLeft < 0 || handleLeft > item.trackWidth) {
                    var amount = void 0;
                    handleLeft = handleLeft < 0 ? 0 : handleLeft > item.trackWidth ? item.trackWidth : handleLeft;
                    amount = handleLeftToAmount(item, handleLeft);
                    updatePreviewColor(item, amountToColor(item, amount));
                }

                item.$handle.css({ left: handleLeft });
                item.$item.off(ENM["mousedown"]).on(ENM["mousedown"], '[data-panel="color-handle"]', function (e) {
                    var mouseObj = getMousePosition(e);
                    item._originalHandleClientX = mouseObj.clientX;
                    item._originalHandleLeft = item.$handle.position().left;
                    handleMoveEvent.on(item);
                    U.stopEvent(e.originalEvent);
                }).off("click").on("click", '[data-panel="color-label"], [data-panel="color-preview"]', function (e) {
                    if (self.onClick) {
                        self.onClick.call(item, '#' + item._selectedColor.toUpperCase(), e);
                    }
                }).on("click", '[data-panel="color-track"]', function (e) {
                    if (e.target.getAttribute("data-panel") == "color-track") {
                        var mouseObj = getMousePosition(e),
                            newHandleLeft = mouseObj.clientX - item.$track.offset().left,
                            _amount2 = handleLeftToAmount(item, newHandleLeft);

                        item.$handle.css({ left: newHandleLeft });
                        updatePreviewColor(item, amountToColor(item, _amount2), e);

                        mouseObj = null;
                        newHandleLeft = null;
                        _amount2 = null;
                    }
                });
            };

            var updatePreviewColor = function updatePreviewColor(item, color, event) {
                item.$preview.css({ "background-color": '#' + color });
                item.$label.html('#' + color.toUpperCase());
                item._selectedColor = color;

                if (event && self.onUpdateColor) {
                    self.onUpdateColor.call(item, '#' + item._selectedColor.toUpperCase());
                }
            };

            var amountToColor = function amountToColor(item, amount) {
                var processor = {
                    "black": function black(_color, _amount) {
                        return _color.lighten(cfg.colors.slider.amount / 2).darken(_amount).getHexValue();
                    },
                    "white": function white(_color, _amount) {
                        return _color.darken(cfg.colors.slider.amount / 2).darken(_amount).getHexValue();
                    },
                    "normal": function normal(_color, _amount) {
                        return _color.darken(_amount).getHexValue();
                    }
                };

                if (item._uniqColor in processor) {
                    return processor[item._uniqColor](item._color, amount);
                } else {
                    return processor["normal"](item._color, amount);
                }
            };

            var colorToAmount = function colorToAmount(item, color) {
                /// todo : 색상에 가까운 색 표현.
                var processor = {
                    "black": function black(_color, _diffColor) {
                        var color1 = _color.lighten(cfg.colors.slider.amount / 2);
                        return (color1.getHsl().l - _diffColor.getHsl().l) * 100;
                    },
                    "white": function white(_color, _diffColor) {
                        var color1 = _color.darken(cfg.colors.slider.amount / 2);
                        return (color1.getHsl().l - _diffColor.getHsl().l) * 100;
                    },
                    "normal": function normal(_color, _diffColor) {
                        return (_color.getHsl().l - _diffColor.getHsl().l) * 100;
                    }
                };

                if (item._uniqColor in processor) {
                    return processor[item._uniqColor](item._color, color);
                } else {
                    return processor["normal"](item._color, color);
                }
            };

            var handleLeftToAmount = function handleLeftToAmount(item, handleLeft) {
                return cfg.colors.slider.amount * (handleLeft - item.trackWidth / 2) / (item.originalTrackWidth / 2);
            };

            var amountToHandleLeft = function amountToHandleLeft(item, amount) {
                return amount * (item.originalTrackWidth / 2) / cfg.colors.slider.amount + item.trackWidth / 2;
            };

            var handleMoveEvent = {
                "on": function on(item) {
                    jQuery(document.body).on(ENM["mousemove"] + ".ax5palette-" + _this.instanceId, function (e) {
                        var mouseObj = getMousePosition(e),
                            da = mouseObj.clientX - item._originalHandleClientX,
                            newHandleLeft = item._originalHandleLeft + da,
                            amount = void 0;

                        newHandleLeft = newHandleLeft < 0 ? 0 : newHandleLeft > item.trackWidth ? item.trackWidth : newHandleLeft;
                        item.$handle.css({ left: newHandleLeft });
                        amount = handleLeftToAmount(item, newHandleLeft);

                        updatePreviewColor(item, amountToColor(item, amount), e);

                        mouseObj = null;
                        da = null;
                    }).on(ENM["mouseup"] + ".ax5palette-" + _this.instanceId, function (e) {
                        handleMoveEvent.off();
                        U.stopEvent(e);
                    }).on("mouseleave.ax5palette-" + _this.instanceId, function (e) {
                        handleMoveEvent.off();
                        U.stopEvent(e);
                    });

                    jQuery(document.body).attr('unselectable', 'on').css('user-select', 'none').on('selectstart', false);
                },
                "off": function off() {
                    self.xvar.resizerLived = false;

                    jQuery(document.body).off(ENM["mousemove"] + ".ax5palette-" + _this.instanceId).off(ENM["mouseup"] + ".ax5palette-" + _this.instanceId).off("mouseleave.ax5palette-" + _this.instanceId);

                    jQuery(document.body).removeAttr('unselectable').css('user-select', 'auto').off('selectstart');
                }
            };

            var repaint = function repaint(selectedColor) {
                var box = {
                    width: _this.$target.innerWidth(),
                    height: _this.$target.innerHeight()
                };

                // 패널 프레임 초기화
                _this.$target.html(PALETTE.tmpl.get("frame", {}, cfg.columnKeys));

                // 각 패널들을 캐싱~
                _this.$ = {
                    "root": _this.$target.find('[data-ax5palette-container="root"]'),
                    "colors": _this.$target.find('[data-ax5palette-container="colors"]'),
                    "controls": _this.$target.find('[data-ax5palette-container="controls"]')
                };

                // todo : controls 나중에 고민하여 구현
                // this.$["controls"].css({height: cfg.controls.height});

                /// colors.list 색상 범위 결정 / 초기화
                cfg.colors.list.forEach(function (c) {
                    c._color = U.color(c.value);
                    c._selectedColor = c._color.getHexValue();
                    if (c._color.r == 0 && c._color.g == 0 && c._color.b == 0) {
                        c._amount = cfg.colors.slider.amount;
                        c._uniqColor = "black";
                        c._color0value = "#" + c._color.lighten(cfg.colors.slider.amount).getHexValue();
                        c._color1value = "#" + c._color.lighten(cfg.colors.slider.amount / 2).getHexValue();
                        c._color2value = "#" + c._color.getHexValue();
                    } else if (c._color.r == 255 && c._color.g == 255 && c._color.b == 255) {
                        c._amount = -cfg.colors.slider.amount;
                        c._uniqColor = "white";
                        c._color0value = "#" + c._color.getHexValue();
                        c._color1value = "#" + c._color.darken(cfg.colors.slider.amount / 2).getHexValue();
                        c._color2value = "#" + c._color.darken(cfg.colors.slider.amount).getHexValue();
                    } else {
                        c._amount = 0;
                        c._color0value = "#" + c._color.lighten(cfg.colors.slider.amount).getHexValue();
                        c._color1value = "#" + c._color.getHexValue();
                        c._color2value = "#" + c._color.darken(cfg.colors.slider.amount).getHexValue();
                    }
                });

                // 색생조절 핸들의 위치 조정cfg.colors.list[minDiffColorIndex]
                cfg.colors.slider.handleLeft = -cfg.colors.slider.handleWidth / 2;
                cfg.colors.slider.handleTop = -cfg.colors.slider.handleHeight / 2;

                // 팔렛트 컬러 패널 초기화
                _this.$["colors"].html(PALETTE.tmpl.get("colors", cfg, cfg.columnKeys));

                _this.$["colors"].find('[data-ax5palette-color-index]').each(function () {
                    var idx = this.getAttribute("data-ax5palette-color-index");
                    var color = cfg.colors.list[idx];
                    var item = jQuery.extend({}, color);
                    item._index = idx;
                    item.$item = jQuery(this);
                    item.$preview = item.$item.find('[data-panel="color"]');
                    item.$label = item.$item.find('[data-panel="color-label"]');
                    item.$track = item.$item.find('[data-panel="color-track"]');
                    item.$handle = item.$item.find('[data-panel="color-handle"]');
                    bindHandle(item);
                    /////
                    self.colors.push(item);
                });

                if (selectedColor) {
                    _this.setSelectedColor(selectedColor);
                }
            };

            /**
             * Preferences of palette UI
             * @method ax5palette.setConfig
             * @param {Object} config
             * @param {Element} config.target
             * @param {String} [config.selectedColor]
             * @param {Object} [config.colors]
             * @param {Object} [config.colors.preview]
             * @param {Number} [config.colors.preview.width=24]
             * @param {Number} [config.colors.preview.height=24]
             * @param {Number} [config.colors.preview.cellWidth=30]
             * @param {Object} [config.colors.label]
             * @param {Number} [config.colors.label.width=80]
             * @param {Object} [config.colors.slider]
             * @param {Number} [config.colors.slider.trackHeight=8]
             * @param {Number} [config.colors.slider.amount=32]
             * @param {Number} [config.colors.slider.handleWidth=18]
             * @param {Number} [config.colors.slider.handleHeight=18]
             * @param {Object[]} [config.colors.list=[red,orange,yellow,green,blue,purple,black,white]]
             * @param {String} config.colors.list[].label
             * @param {String} config.colors.list[].value
             * @param {Object} [config.controls]
             * @param {Number} [config.controls.height=0]
             * @returns {ax5palette}
             * @example
             * ```js
             * myPalette = new ax5.ui.palette({
             *  target: $('[data-ax5palette="01"]'),
             *  onClick: function (hexColor) {
             *      alert(hexColor);
             *  }
             * });
             *
             * myPalette = new ax5.ui.palette({
             *  target: $('[data-ax5palette="01"]'),
             *  colors: {
             *      list: [
             *          {label: "red", value: "#ff0000"},
             *          {label: "orange", value: "#ff9802"},
             *          {label: "yellow", value: "#ffff00"},
             *          {label: "skyblue", value: "#84e4ff"},
             *          {label: "white", value: "#ffffff"}
             *      ]
             *  }
             *  onClick: function (hexColor) {
             *  }
             * });
             * ```
             */
            this.init = function () {
                // after setConfig();
                this.onStateChanged = cfg.onStateChanged;
                this.onClick = cfg.onClick;
                this.onUpdateColor = cfg.onUpdateColor;

                if (!cfg.target) {
                    console.log(ax5.info.getError("ax5palette", "401", "setConfig"));
                }
                this.$target = jQuery(cfg.target);

                setTimeout(function () {
                    repaint((cfg.selectedColor || "").trim()); // 팔렛트 그리기.
                });
            };

            /**
             * @method ax5palette.repaint
             * @returns {ax5palette}
             */
            this.repaint = function () {
                repaint();
                return this;
            };

            /**
             * @method ax5palette.setSelectedColor
             * @param selectedColor
             * @returns {ax5palette}
             */
            this.setSelectedColor = function (selectedColor) {

                var sColor = U.color(selectedColor.trim());
                // 지정된 색이 가장 가까운 파렛 검색
                var minDiffColor = 255 * 3,
                    minDiffColorIndex = -1;

                self.colors.forEach(function (c, cidx) {
                    var c1hsl = c._color.getHsl(),
                        c2hsl = sColor.getHsl();
                    var diffColor = Math.abs(c1hsl.h - c2hsl.h) + Math.abs(c1hsl.s - c2hsl.s) + Math.abs(c1hsl.l - c2hsl.l);
                    if (diffColor < minDiffColor) {
                        minDiffColor = diffColor;
                        minDiffColorIndex = cidx;
                    }
                });

                if (minDiffColorIndex > -1) {
                    var amount = void 0,
                        handleLeft = void 0,
                        item = self.colors[minDiffColorIndex];

                    item._amount = colorToAmount(item, sColor);
                    handleLeft = amountToHandleLeft(item, item._amount);
                    //handleLeft = handleLeft < 0 ? 0 : handleLeft > item.trackWidth ? item.trackWidth : handleLeft;
                    item.$handle.css({ left: handleLeft });

                    amount = handleLeftToAmount(item, handleLeft);
                    updatePreviewColor(item, amountToColor(item, amount));
                }

                return this;
            };

            // 클래스 생성자
            this.main = function () {

                UI.palette_instance = UI.palette_instance || [];
                UI.palette_instance.push(this);

                if (arguments && U.isObject(arguments[0])) {
                    this.setConfig(arguments[0]);
                }
            }.apply(this, arguments);
        };
    }());

    PALETTE = ax5.ui.palette;
})();
// ax5.ui.calendar.tmpl
(function () {

    var PALETTE = ax5.ui.palette;

    var tmpl_frame = function tmpl_frame(columnKeys) {
        return "\n
\n

\n

\n
\n";
    };

    var tmpl_colors = function tmpl_colors(columnKeys) {
        return "\n{{#colors}}\n{{#list}}\n
\n
\n

\n
\n
{{label}}
\n
\n
\n
\n

\n
\n
\n
\n
\n{{/list}}\n{{/colors}}\n";
    };

    PALETTE.tmpl = {
        "frame": tmpl_frame,
        "colors": tmpl_colors,

        get: function get(tmplName, data, columnKeys) {
            return ax5.mustache.render(PALETTE.tmpl[tmplName].call(this, columnKeys), data);
        }
    };
})();


'use strict';

var httpProxy    = require('http-proxy'),
    util         = require('util'),
    url          = require('url'),
    configParser = require('./config');

var routers = {},
    logger,
    config,
    useGateway;

exports.initialize = function initialize(options){
  logger = options.logger || require('./logger');
  config = configParser(options).proxy;
  useGateway = (config.gateway !== null && typeof(config.gateway) === 'object' && typeof(config.gateway.host) === 'string');

  // called on every request
  // decides if the request should be handled locally or forwarded to a remote server
  // based on the forwarding ruleset
  return function jsonProxy(req, res, next) {
    var match = config.forward.some(function(rule) {
      if (rule.regexp.test(req.url)) {
        proxyRequest(req, res, rule);
        return true;
      }
    });

    // fall through to the next middleware if there wasn't a proxy match rule
    if (match === false) {
      next();
    }
  };
};

// injects any LAN proxy servers into the request
function proxyRequest(req, res, rule) {
  var router,
      target,
      path;

  injectProxyHeaders(req, rule);

  // rewrite the base path of the requested URL
  path = req.url.replace(rule.regexp, rule.target.path);

  if (useGateway) {
    // for HTTP LAN proxies, rewrite the request URL from a relative URL to an absolute URL
    // also add a header that can be inspected by the unit tests
    req.url = url.parse(util.format('%s//%s:%s%s', rule.target.protocol, rule.target.host, rule.target.port, path)).href;
    req.headers['X-Forwarded-Url'] = req.url;

    // the proxy target is really the HTTP LAN proxy
    target = config.gateway;
    logger.info('proxy: %s %s --> %s:%s --> %s//%s:%s%s', req.method, req.url, config.gateway.host, config.gateway.port, rule.target.protocol, rule.target.host, rule.target.port, path);
  } else {
    target = rule.target;
    logger.info('proxy: %s %s --> %s//%s:%s%s', req.method, req.url, rule.target.protocol, rule.target.host, rule.target.port, path);
    req.url = path;
  }

  var errorCallback = function errorCallback(err, proxyRequest, proxyResponse) {
    var status = 500;
    if (proxyResponse !== undefined && proxyResponse !== null && proxyResponse.statusCode >= 400) {
      status = proxyResponse.statusCode;
    }

    logger.error('proxy: error - %s %s - %s', proxyRequest.method, proxyRequest.url, err.message);
    if( res.status && typeof res.status === 'function'  ){
        res.status(status).json({ error: status, message: err.message });
    }
  };

  // get a ProxyServer from the cache
  router = createRouter(target);

  // proxy the request
  router.web(req, res, errorCallback);
}

// factory method to cache/fetch HttpProxy instances
function createRouter(target) {
  var key = util.format('%s//%s:%s', target.protocol, target.host, target.port),
      router = routers[key],
      options;

  // httpProxy.createProxyServer options
  // {
  //   target : <url string to be parsed with the url module>
  //   forward: <url string to be parsed with the url module>
  //   agent  : <object to be passed to http(s).request>
  //   ssl    : <object to be passed to https.createServer()>
  //   ws     : <true/false, if you want to proxy websockets>
  //   xfwd   : <true/false, adds x-forward headers>
  //   secure : <true/false, verify SSL certificate>
  //   toProxy: passes the absolute URL as the path (useful for proxying to proxies)
  // }

  if (router === undefined || router === null) {
    options = {
      xfwd: true,
      secure: (target.protocol && target.protocol === 'https://'),
      target: key,
      prependPath: (useGateway === true),
      toProxy: (useGateway === true)
    };

    router = httpProxy.createProxyServer(options);

    routers[key] = router;
  }

  return router;
}

// support basic auth for LAN HTTP proxied connections, if needed
// HACK: http-proxy uses node's http.request().pipe(), which doesn't properly
//       support the options.auth setting like http.request.write() as of Node v0.10.24,
//       so this copies the implementation of request.write() from http.request()
// SOURCE: https://github.com/joyent/node/blob/828f14556e0daeae7fdac08fceaa90952de63f73/lib/_http_client.js#L84-L88
function injectAuthHeader(req) {
  if (useGateway === true && typeof(config.gateway.auth) === 'string' && req.headers['authorization'] === undefined) {
    req.headers['authorization'] = 'Basic ' + new Buffer(config.gateway.auth).toString('base64');
  }
}

// inject any custom header values into a proxy request
// along with the x-forwarded-for, x-forwarded-port, and via headers
function injectProxyHeaders(req, rule){
  // the HTTP host header is often needed by the target webserver config
  req.headers['host'] = rule.target.host + (rule.target.originalPort ? util.format(':%d', rule.target.originalPort) : '');
  // document that this request was proxied
  req.headers['via'] =  util.format('http://%s:%s', req.connection.address().address, req.connection.address().port);

  // inject any custom headers as configured
  config.headers.forEach(function(header) {
    var value = header.value,
        name = header.name;

    if(typeof(value) === 'function') {
      value = value.call(undefined, req);
    }

    if(typeof(value) !== 'string') {
      value = '';
    }

    if (typeof(name) === 'string') {
      req.headers[name.toLowerCase()] = value;
    }
  });

  injectAuthHeader(req);
}