Source: manager/controllerManager.js


define(['mmirf/controller', 'mmirf/resources', 'mmirf/commonUtils', 'mmirf/util/deferred', 'mmirf/logger', 'module' ],

	/**
	 * A class for managing the controllers of the application. <br>
	 * It's purpose is to load the controllers and their views / partials and provide functions to find controllers or
	 * perform actions or helper-actions.
	 *
	 * This "class" is structured as a singleton - so that only one instance is in use.<br>
	 *
	 *
	 * @class
	 * @name mmir.ControllerManager
	 * @static
	 * @hideconstructor
	 *
	 */
	function(
		Controller, res, commonUtils, deferred, Logger, module
){
	//the next comment enables JSDoc2 to map all functions etc. to the correct class description
	/** @scope mmir.ControllerManager.prototype */


	/**
	 * The logger for the ControllerManager.
	 *
	 * @private
	 * @memberOf mmir.ControllerManager
	 */
	var logger = Logger.create(module);//initialize with requirejs-module information

	/**
	 * Initialize ControllerManager:
	 *
	 * Load all Controllers from /controller
	 * that are specified in /gen/directories.json
	 *
	 * @function
	 * @param {Function} [callback] OPTIONAL
	 * 				an optional callback that will be triggered after the controllers where loaded
	 * @param {Object} [ctx] OPTIONAL
	 * 				the context for the controller & helper implementations (DEFAULT: the global context, i.e. window)
	 * @returns {Promise}
	 * 				a Deferred promise that will get fulfilled when controllers are loaded
	 * @private
	 *
	 * @memberOf mmir.ControllerManager#
	 */
	function _init(callback, ctx, _instance, ctrlList) {

		//shift arguments if necessary:
		if(!ctx && typeof callback !== 'function'){
			ctx = callback;
			callback = void(0);
		}

		//set ctx to global/window, if not already set:
		ctx = ctx || (typeof window !== 'undefined' ? window : typeof self !== 'undefined' ? self : typeof global !== 'undefined' ? global : this);

		//create return value
		var defer = deferred();
		if(callback){
			defer.then(callback, callback);
		}


		/**
		 * HELPER FUNC: remove file extension from file-name
		 * @private
		 *
		 * @memberOf mmir.ControllerManager#
		 */
		function removeFileExt(fileName){
			return fileName.replace(/\.[^.]+$/g,'');
		}
		/**
		 * HELPER FUNC: convert first letter to upper case
		 * @private
		 *
		 * @memberOf mmir.ControllerManager#
		 */
		function firstToUpperCase(name){
			return name[0].toUpperCase()+name.substr(1);
		}

		/**
		 * HELPER FUNC: add file path for generated / compiled view-element
		 *              if it exists.
		 *
		 * @param {String} genDirPath
		 * 				path the the directory, where the file for the generated view-element
		 * 				is potentially located (generated file may not exists)
		 *
		 * @param {PlainObject} infoObj
		 * 				the info-object for the view-element. MUST HAVE property <code>name</code>!
		 *
		 * @param {String} [fileNamePrefix] OPTIONAL
		 * 				prefix for the file-name, e.g. in case of Partials, the file-name would be:
		 * 				<code>fileNamePrefix + infoObj.name</code> (+ file-extension)
		 *
		 * @returns {PlainObject}
		 * 				the info-object: if a path for the generated file exists,
		 * 				a property <code>genPath</code> (String) with the path as value is added.
		 * @private
		 *
		 * @memberOf mmir.ControllerManager#
		 */
		function addGenPath(genDirPath, infoObj, fileNamePrefix){

			if(infoObj.genPath){
				return infoObj;
			}

			var prefix = fileNamePrefix? fileNamePrefix : '';
			var filter = prefix + infoObj.name + '.js';
			var path = genDirPath + '/';
			if(typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD){
				path = '';
				filter = new RegExp('^mmirf/view/[^/]+/'+filter+'$', 'i');
			}
			var genPath = commonUtils.listDir(genDirPath, filter);
			if(genPath && genPath.length > 0){
				infoObj.genPath = path + genPath[0];
			}

			return infoObj;
		}

		/**
		 * HELPER FUNC: parse file list and extract info for view/partial/layout/helper.
		 *
		 * @param {String} dirPath
		 * 				path the the directory, where the (source) file is located,
		 * 				e.g. eHTML template or JS helper implementation
		 *
		 * @param {String} genDirPath
		 * 				path the the directory, where the file for the generated view-element
		 * 				is potentially located (generated file may not exists)
		 *
		 * @param {QueryFilter} queryFilter
		 * 				the filter for querying (i.e. filtering) <code>commonUtils.listDir()</code>:
		 * 					queryFilter.nameStart: {String} an regular expression that the file-name must match (or empty string, if all file-names should match)
		 * 					queryFilter.ext: {String} the file-extension that should match, with dot e.g. "js" or "ehtml"
		 *
		 * @param {String} [removeNamePrefix] OPTIONAL
		 * 				prefix of the file-name which should be removed when extracting the InfoObj.name, e.g. in case of Partials:
		 * 				<code>removeFileExt(fileName) === removeNamePrefix + infoObj.name</code> (+ file-extension)
		 *
		 * @param {RegExp} [regExpFileFilter] OPTIONAL
		 * 				regular expression for filtering the fileList, i.e. only files that match the expression will be
		 * 				included in the returned list of FileInfo objects
		 *
		 * @returns {Array<FileInfo>}
		 * 				list of file-info objects:
		 * 					FileInfo.name: {String} the name that will be used to identify the resource
		 * 					FileInfo.path: {String} the path to the source file, e.g. for loading the resource
		 * 					FileInfo.genPath: {String} the path to generated/executable resource
		 * @private
		 *
		 * @memberOf mmir.ControllerManager#
		 */
		function processFileList(dirPath, genDirPath, queryFilter, removeNamePrefix, regExpFileFilter){

			if(removeNamePrefix && typeof removeNamePrefix === 'object'){
				regExpFileFilter = removeNamePrefix;
				removeNamePrefix = '';
			} else {
				removeNamePrefix = removeNamePrefix || '';
			}

			var isSourceList = true;
			var queryFileName = queryFilter.nameStart? '^' + queryFilter.nameStart + '.*' : '';
			var fileList = commonUtils.listDir(dirPath, new RegExp(queryFileName + '\\.' + queryFilter.ext + '$', 'i'));
			var wpBuild = typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD;
			if(!fileList){
				// -> query gen-dir instead
				isSourceList = false;
				if(wpBuild){
					//NOTE in webpack build the entry is not a file-name, but a module ID, like "mmirf/view/application/login.js"
					queryFileName = queryFilter.nameStart? '^mmirf/view/[^/]+/' + queryFilter.nameStart + '[^/]*' : '';;
				}
				//NOTE file-extension in gen-dir is always .js
				fileList = commonUtils.listDir(genDirPath, new RegExp(queryFileName+ '\\.js$', 'i'));
			}

			var infoList = [], entry, name;
			if (fileList != null) {
				for (var i = 0, size = fileList.length; i < size; ++i) {
					entry = fileList[i];
					if(!regExpFileFilter || regExpFileFilter.test(entry)){
						name = removeFileExt(!removeNamePrefix? entry : entry.replace(removeNamePrefix, ''));
						infoList.push(addGenPath(genDirPath, {
							name: wpBuild? name.replace(/^.*\//, '') : name,													//<- in webpack build: remove everything from ID (i.e. the name) except for the last segement
							path: isSourceList? (wpBuild? entry : dirPath + '/' + entry) : null,
							genPath: isSourceList? null : (wpBuild? entry : genDirPath + '/' + entry)	//<- in webpack build the genPath is the module ID, otherwise it's the path to the generated file
						}, removeNamePrefix));
					}
				}
			}
			return infoList;
		}

		/**
		 * HELPER get first entry from FileInfo list and transform its name (i.e. so that first letter is upper case)
		 *
		 * @param  {Array<FileInfo>} infoList the list of FileInfo objects
		 * @param  {String} type the type of resources listed in infoList, e.g. "layout" or "helper"
		 * @return {FileInfo} the first FileInfo object or NULL
		 */
		function getFirstInfo(infoList, type){
			var len = infoList.length;
			if(len > 1){
				logger.warn('Invalid number of '+type+': only 1 is allowed, using first of list ...');
			}
			if(len >= 1){
				var info = infoList[0];
				info.name = firstToUpperCase(info.name);
				return info;
			}
			return null;
		}

		/**
		 * This function gets the controller file names and builds a JSON object containing information about
		 * the location, file name etc. for the controller itself, its views, partials, layout, and helper.
		 *
		 * @function
		 * @param {String} controllerName
		 * 					the name of the Controller (must start with an upper case letter).
		 * @param {String} controllerPath
		 * 					the path (URL) where the file with the Controller's implementation
		 * 					is located (according to information in file <code>/gen/directories.json</code>,
		 * 					i.e. {@link mmir.CommonUtils#getDirectoryStructure})
		 * @returns {JSON} JSON-Object containing information about the controller,
		 * 				its views, partials, and paths etc.
		 * @private
		 *
		 * @example
		 * //EXAMPLE for returned object:
		 * {
		 *   "fileName": "application",
		 *   "name": "Application",
		 *   "path": "controllers/application.js",
		 *   "views": [
		 *     {
		 *       "name": "login",
		 *       "path": "views/application/login.ehtml",
		 *       "genPath": "gen/view/application/login.js"
		 *     },
		 *     {
		 *       "name": "registration",
		 *       "path": "views/application/registration.ehtml",
		 *       "genPath": "gen/view/application/registration.js"
		 *     },
		 *     {
		 *       "name": "welcome",
		 *       "path": "views/application/welcome.ehtml",
		 *       "genPath": "gen/view/application/welcome.js"
		 *     }
		 *   ],
		 *   "partials": [
		 *     {
		 *       "name": "languageMenu",
		 *       "path": "views/application/~languageMenu.ehtml",
		 *       "genPath": "gen/view/application/~languageMenu.js"
		 *     }
		 *   ],
		 *   "helper": {
		 *     "name": "ApplicationHelper",
		 *     "path": "helpers/applicationHelper.js",
		 *     "genPath": "helpers/applicationHelper.js"
		 *   },
		 *   "layout": {
		 *     "name": "application",
		 *     "path": "views/layouts/application.ehtml",
		 *     "genPath": "gen/view/layouts/application.js"
		 *   }
		 * }
		 * //NOTE: layout and helper may be NULL
		 *
		 * @requires mmir.CommonUtils
		 * @requires mmir.Resources
		 *
		 * @memberOf mmir.ControllerManager#
		 */
		function getControllerResources(controllerName, controllerPath){

			var partialsPrefix = commonUtils.getPartialsPrefix();
			var controllerFilePath = controllerPath + controllerName;

			var rawControllerName= removeFileExt(controllerName);
			controllerName = rawControllerName;

			var viewsPath = res.getViewPath() + controllerName;
			var genViewsPath = res.getCompiledViewPath() + controllerName;

			controllerName = firstToUpperCase(controllerName);

			var viewsList = processFileList(viewsPath, genViewsPath, {nameStart: '(?!'+partialsPrefix+')', ext: 'ehtml'});

			var partialsInfoList = processFileList(viewsPath, genViewsPath, {nameStart: partialsPrefix, ext: 'ehtml'}, partialsPrefix);

			var helpersPath = res.getHelperPath().replace(/\/$/, '');//<- remove trailing slash;
			var helperSuffix = res.getHelperSuffix();
			var reStartPattern = typeof WEBPACK_BUILD !== 'undefined' && WEBPACK_BUILD? '^mmirf/helper/' : '^';
			var reHelpersFileName = new RegExp(reStartPattern+controllerName+helperSuffix+'\.js$', 'i');
			var helpersList = processFileList(helpersPath, helpersPath, {nameStart: '', ext: 'js'}, reHelpersFileName);
			var helperInfo = getFirstInfo(helpersList, 'helper');

			var layoutsPath = res.getLayoutPath().replace(/\/$/, '');//<- remove trailing slash
			var layoutGenPath = res.getCompiledLayoutPath().replace(/\/$/, '');//<- remove trailing slash
			var reStartsWithCtrl = new RegExp('^'+controllerName, 'i');
			var layoutsList = processFileList(layoutsPath, layoutGenPath, {nameStart: '(?!'+partialsPrefix+')', ext: 'ehtml'}, reStartsWithCtrl);
			var layoutInfo = getFirstInfo(layoutsList, 'layout');


			var ctrlInfo = {
				fileName: rawControllerName,
				name:     controllerName,
				path:     controllerFilePath,

				views:    viewsList,
				partials: partialsInfoList,
				helper:   helperInfo,
				layout:   layoutInfo
			};

			// //TEST compare info with "reference" result from original impl.:
			// var test = {
			// 	application: '{"fileName":"application","name":"Application","path":"controllers/application","views":[{"name":"login","path":"views/application/login.ehtml"},{"name":"registration","path":"views/application/registration.ehtml"},{"name":"welcome","path":"views/application/welcome.ehtml"}],"partials":[{"name":"languageMenu","path":"views/application/~languageMenu.ehtml"}],"helper":null,"layout":null}',
			// 	calendar: '{"fileName":"calendar","name":"Calendar","path":"controllers/calendar","views":[{"name":"create_appointment","path":"views/calendar/create_appointment.ehtml"}],"partials":[],"helper":null,"layout":null}'
			// };
			// var isEqual = (JSON.stringify(ctrlInfo) === test[ctrlInfo.fileName]);
			// console[isEqual? 'info':'error']('compliance-test: isEual? '+  isEqual);
			// console.log(' ######## ctrlInfo -> ', JSON.stringify(ctrlInfo));

			return ctrlInfo;
		};

		/**
		 * HELPER create the controller instance after its implementation was loaded/is available in ctx or via argument result
		 *
		 * After the controller instance was initialized, it is added to ctrlList (with its name as key)
		 *
		 * @param  {String} fileName the name of the controller's file: application.js -> Controller<Appliction>
		 * @param  {any|Function} result the result of loading the controller implementation, i.e. its constructor function; if undefined, the constructor must be available in ctx in ctx[<controller name>]
		 *
		 * @memberOf mmir.ControllerManager#
		 */
		function createCtrlInstance(fileName, result){

			var ctrlInfo = getControllerResources(fileName, res.getControllerPath());

			var constr = ctx[ctrlInfo.name];

			//FIXME HACK for handling exported constructor
			if(typeof result === 'function' && result.name === ctrlInfo.name){
				constr = result;
			}

			var controller = new Controller(ctrlInfo.name, ctrlInfo, constr);

			if(ctrlInfo.helper){
				var helperPath = ctrlInfo.helper.path;
				var helperName = ctrlInfo.helper.name;
				controller.loadHelper(helperName,helperPath, ctx);
			}

			ctrlList.set(controller.getName(), controller);

			logger.info('[loadController] "'+fileName+'" loaded.');
		};

		commonUtils.loadImpl(


			res.getControllerPath(),

			false,

			function () {

				console.info( '[loadControllers] done' );

				defer.resolve(_instance);
			},

			function isAlreadyLoaded (name) {

				if(typeof ctx[name] === 'function' && ctx[name].name === firstToUpperCase(name)){
					//controller implementation was already loaded -> immediately create controller instance
					if(logger.isVerbose()) logger.v("already loaded implementation for "+name+", creating instance...");//debug
					createCtrlInstance(name, ctx[name]);
					return true;
				}
				return false;

			},

			function callbackStatus(status, fileName, msg, result) {
				if(status==='info'){

					createCtrlInstance(fileName, result);
				}
				else if(status==='warning'){
					logger.warn('[loadController] "'+fileName+'": '+msg);
				}
				else if(status==='error'){
					logger.error('[loadController] "'+fileName+'": '+msg);
				}
				else{
					logger.error('[loadController] '+status+' (UNKNOWN STATUS) -> "'+fileName+'": '+msg);
				}
			}

		);

		return defer;

	};

	/**
	 * create ControllerManager instance
	 *
	 * @type controllerManagerFactory
	 * @private
	 *
	 * @memberOf mmir.ControllerManager#
	 */
	var _create = function(){

		// private members
		/**
		 * Array of controller-instances
		 *
		 * @type Map
		 * @private
		 *
		 * @memberOf mmir.ControllerManager#
		 */
		var controllers = new Map();

		/**
		 * Object containing the instance of the class {@link mmir.ControllerManager}
		 *
		 * @type Object
		 * @private
		 * @augments mmir.ControllerManager
		 * @ignore
		 */
		var _instance = {
			/** @scope mmir.ControllerManager.prototype *///for jsdoc2

			// public members

			/**
			 * This function gets the controller by name.
			 *
			 * @function
			 * @param {String} ctrlName Name of the controller which should be returned
			 * @returns {mmir.ctrl.Controller} controller if found, null else
			 * @public
			 * @memberOf mmir.ControllerManager.prototype
			 */
			get: function(ctrlName){
				var ctrl = controllers.get(ctrlName);
				if(!ctrl){
					return null;
				}
				return ctrl;
			},


			/**
			 * This function returns names of all loaded controllers.
			 *
			 * @function
			 * @returns {Array<String>} Names of all loaded controllers
			 * @public
			 * @memberOf mmir.ControllerManager.prototype
			 */
			getNames: function(){

				return Array.from(controllers.keys());
			},


			/**
			 * This function performs an action of a controller.
			 *
			 * @function
			 * @param {String} ctrlName Name of the controller to which the action belongs
			 * @param {String} actionName Name of the action that should be performed
			 * @param {Object} data optional data that can be submitted to the action
			 * @returns {Object} the return object of the performed action
			 * @public
			 * @memberOf mmir.ControllerManager.prototype
			 */
			perform: function(ctrlName, actionName, data){
				var ctrl = this.get(ctrlName);
				if (ctrl != null) {
					return ctrl.perform(actionName, data);
				}
				else {
					console.error('ControllerManager.perform: the controller could not be found "'+ctrlName+'"');
				}
			},


			/**
			 * This function performs an action of a helper-class for a controller.
			 *
			 * @function
			 * @param {String} ctrlName Name of the controller to which the helper action belongs
			 * @param {String} actionName Name of the action that should be performed by the helper
			 * @param {Object} data optional data that can be submitted to the action
			 * @returns {Object} the return object of the performed action
			 * @public
			 * @memberOf mmir.ControllerManager.prototype
			 */
			performHelper: function(ctrlName, actionName, data) {

				var ctrl = this.get(ctrlName);
				if (ctrl != null) {
					if(arguments.length > 3){
						return ctrl.performHelper(actionName, data, arguments[3]);
					}
					else {
						return ctrl.performHelper(actionName, data);
					}
				}
				else {
					console.error('ControllerManager.performHelper: the controller could not be found "'+ctrlName+'"');
				}
			},
			/**
			 * This function must be called before using the {@link mmir.ControllerManager}. The Initialization process is asynchronous,
			 * because javascript-files must be loaded (the controllers).
			 * To ensure that the ControllerManager is initialized, a callback can be used, or the returned
			 * <em>Promise</em> (i.e. a "then-able" object) for code, that relies
			 * on the presence of the loaded controllers.<br>
			 *
			 *
			 * <div class="box important">
			 * <b>Note:</b>
			 * The callback function should be used for code, that requires the prior loading of the controllers.<br>
			 * The callback mechanism is necessary, because loading the controllers is asynchronous.<br><br>
			 * If provided, the callback function is invoked with 1 argument, the ControllerManager instance:<br>
			 * <code> callbackFunction(controllerManagerInstance) </code>
			 * </div>
			 *
			 * @function
			 * @async
			 *
			 * @param {Function} [callback] OPTIONAL
			 * 				an optional callback that will be triggered after the controllers where loaded
			 * @param {Object} [ctx] OPTIONAL
			 * 				the context for the controller & helper implementations (DEFAULT: the global context, i.e. window)
			 * @returns {Promise}
			 * 				a deferred promise that will get fulfilled when controllers are loaded
			 * @example
			 *  //recommended style:
			 *  mmir.require(['mmirf/controllerManager', ...], function(controllerManager, ...) {
			 *  	controllerManager.init().then(function(theInitializedControllerInstance){
			 *  		...
			 *  	});
			 *  })
			 *
			 *  //old style:
			 * 	function afterLoadingControllers(controllerManagerInstance){
			 * 		var appCtrl = controllerManagerInstance.get('Application');
			 * 		//do something...
			 * 	}
			 * 	mmir.ctrl.init(afterLoadingControllers);
			 * @public
			 * @memberOf mmir.ControllerManager.prototype
			 */
			init: function(callback, ctx){
				return _init(callback, ctx, _instance, controllers);
			},
			_create: _create

		};

		/**@ignore*/
		return _instance;
	};


	return _create();
});