1 /*
  2  * 	Copyright (C) 2012-2013 DFKI GmbH
  3  * 	Deutsches Forschungszentrum fuer Kuenstliche Intelligenz
  4  * 	German Research Center for Artificial Intelligence
  5  * 	http://www.dfki.de
  6  * 
  7  * 	Permission is hereby granted, free of charge, to any person obtaining a 
  8  * 	copy of this software and associated documentation files (the 
  9  * 	"Software"), to deal in the Software without restriction, including 
 10  * 	without limitation the rights to use, copy, modify, merge, publish, 
 11  * 	distribute, sublicense, and/or sell copies of the Software, and to 
 12  * 	permit persons to whom the Software is furnished to do so, subject to 
 13  * 	the following conditions:
 14  * 
 15  * 	The above copyright notice and this permission notice shall be included 
 16  * 	in all copies or substantial portions of the Software.
 17  * 
 18  * 	THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS 
 19  * 	OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 
 20  * 	MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 
 21  * 	IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY 
 22  * 	CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, 
 23  * 	TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 
 24  * 	SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
 25  */
 26 
 27 /**
 28  * Factory for creating the state-machine's <em>raise()</em> function.
 29  * 
 30  * Creates implementation for the state-engine's
 31  * <pre>
 32  * {
 33  * 		name: STRING        // engine name / description
 34  * 		doc: STRING         // the URL to the SCXML document that holds the engine's definition
 35  * 		evalScript: BOOLEAN // true
 36  * 		onraise: FUNCTION   // default impl. for onraise: print debug output (depending on logging-level for DialogEngine / InputEngine)
 37  * 		onload: FUNCTION    // default impl. handling initialization of state-engine after SCXML definition was loaded
 38  * 		raise: FUNCTION     // default impl. for raise-function: use execution-queue for allowing raise-invocations from within SCXML scripts
 39  * }
 40  * </pre>
 41  * 
 42  * @class mmir.env.statemachine.engine.exec
 43  * 
 44  * @requires jQuery.extend
 45  * @requires cordova.plugins.queuePlugin: needed for Android < 4.4 -> Cordova plugin for non-WebWorker-based execution-queue
 46  */
 47 define(['constants', 'scionEngine', 'jquery'], function(constants, createScionEngine, $) {
 48 
 49 	/**
 50 	 * HELPER logging for state-changes
 51 	 * 
 52 	 * @param {Engine} ctx
 53 	 * 				the context for the state-machine, i.e. the DialogEngine or InputEngine instance
 54 	 * 
 55      * @memberOf mmir.env.statemachine.engine.exec#
 56 	 */
 57 	var printDebugStates = function(ctx){
 58 		if(!ctx._log.isDebug()){
 59 			return;
 60 		}
 61 		ctx._log.debug(ctx.name, 'current state: ' + JSON.stringify(ctx.getStates()));
 62 		ctx._log.debug(ctx.name, 'active states: ' + JSON.stringify(ctx.getActiveStates()));
 63 		ctx._log.debug(ctx.name, 'active events: ',+ JSON.stringify(ctx.getActiveEvents()));
 64 		ctx._log.debug(ctx.name, 'active transitions: '+ JSON.stringify(ctx.getStates()) + ":"+ JSON.stringify(ctx.getActiveTransitions()));
 65 	};
 66 
 67 	/**
 68 	 * Factory for default implementation for state-engine, returns
 69 	 * <pre>
 70 	 * {
 71 	 * 		name: STRING        // engine name / description
 72 	 * 		doc: STRING         // the URL to the SCXML document that holds the engine's definition
 73 	 * 		evalScript: BOOLEAN // true
 74 	 * 		onraise: FUNCTION   // default impl. for onraise: print debug output (depending on logging-level for DialogEngine / InputEngine)
 75 	 * 		onload: FUNCTION    // default impl. handling initialization of state-engine after SCXML definition was loaded
 76 	 * 		raise: FUNCTION     // raise-function created by envFactory(_instance)
 77 	 * }
 78 	 * </pre>
 79 	 * 
 80 	 * @param {Engine} _engine
 81 	 * 				the instance of the SCION state-machine
 82 	 * 
 83 	 * @param {ExecFactory} envFactory
 84 	 * 				the factory for creating the Worker/Thread and the raise() function
 85 	 * 
 86 	 * @returns the implementation with raise-function
 87 	 * 
 88      * @memberOf mmir.env.statemachine.engine.exec#
 89 	 */
 90     var _defaultFactory = function(_instance, envFactory){ /** @class StateEngineDefaultImpl */ return {
 91     		/** @scope  StateEngineDefaultImpl */
 92 
 93     		/** @memberOf  StateEngineDefaultImpl */
 94     		name : envFactory.name? envFactory.name : 'default_engine',
 95 
 96     		doc : null,
 97 
 98     		onraise : function() {
 99 
100     			if (this._log.isd()) {
101     				printDebugStates(_instance);
102     			};
103 
104     		},
105 
106     		evalScript : true,
107 
108     		onload : function(scion, deferred) {
109 
110     			//FIX (russa) for jQuery > 2.x: extend() uses isPlainObject(), which's impl. changed in 2.x
111     			//                  -> isPlainObject() now requires a constructor that is different from the native Object.constructor
112     			//					   in order to be able to detect, that an object is NOT a plain object
113     			//					   ... but scion does not provide such a non-native constructor for its _scion property
114     			//					   (which causes deep-copy in extend() to run into an infinite loop)
115     			// QUICK-FIX: just attach a dummy constructor function, so that isPlainObject will correctly detect property
116     			//            _scion as not-a-plain-object (this should really be FIXed in the scion library itself...)
117     			//
118     			//TODO russa: check if we really need a deep copy here (maybe we should make a copy TO scion and replace _instance with the ext. scion obj. ...?)
119     			scion['_scion'].constructor = function dummy(){};
120     			$.extend(true, _instance, scion);
121 
122     			_instance.worker = envFactory.createWorker(_instance, _instance.gen);//_instance._genFuncFactory(_instance, _instance.gen);
123 
124     			//FIXME @russa: is this a general functionality? should this be removed?
125     			if (!_instance.evalScript){
126     				_instance.ignoreScript();
127     			}
128 
129     			// delete _instance.gen;
130     			delete _instance.evalScript;
131 
132     			_instance.start();
133 
134     			deferred.resolve(_instance);
135     			
136     		},//END: onload
137     		
138     		raise: envFactory.createRaise(_instance)
139     	};
140     };
141     
142     /**
143 	 * Factory for WebWorker-based implementation of raise function.
144 	 * 
145 	 * Provide creator-functions:
146 	 * <code>createWorker(_engineInstance, genFunc) : WebWorker</code>
147 	 * <code>createRaise(_engineInstance) : Function</code>
148 	 *  
149      * @memberOf mmir.env.statemachine.engine.exec#
150 	 */
151     var _browserFactory = {
152 		/** @scope  StateEngineWebWorkerImpl */
153 
154 		/** @memberOf  StateEngineWebWorkerImpl */
155     	name: 'webworkerGen',
156 		createWorker: function(_instance, gen) {
157 
158 			var scionQueueWorker = new Worker(
159 					constants.getWorkerPath()+ 'ScionQueueWorker.js'
160 			);
161 			scionQueueWorker._engineInstance = _instance;
162 			scionQueueWorker._engineGen = gen;
163 
164 			scionQueueWorker.onmessage = function(e) {
165 
166 				if (e.data.command == "toDo") {
167 					var inst = this._engineInstance;
168 					inst._log.debug('raising:' + e.data.toDo.event);
169 
170 					this._engineGen.call(inst, e.data.toDo.event, e.data.toDo.eventData);
171 
172 					// @chsc03: attention if something goes wrong along the transition,
173 					// the worker is not ready for any incoming jobs
174 					this.postMessage({
175 						command : 'readyForJob'
176 					});
177 
178 					inst.onraise();
179 				}
180 			};
181 
182 			return scionQueueWorker;
183 
184 		},
185 		createRaise: function(_instance){
186 			return function(event, eventData) {
187 
188 				if (eventData){
189 					this._log.debug('new Job:' + event);
190 				}
191 
192 				_instance.worker.postMessage({
193 					command : 'newJob',
194 					job : {
195 						event : event,
196 						eventData : eventData
197 					}
198 				});
199 			};
200 		}
201 
202     };
203     
204     /**
205 	 * Factory for Android-based implementation of raise function.
206 	 * 
207 	 * Provide creator-functions:
208 	 * <code>createWorker(_engineInstance, genFunc) : WebWorker</code>
209 	 * <code>createRaise(_engineInstance) : Function</code>
210 	 * 
211 	 * 
212 	 * @requires cordova.plugins.queuePlugin (Cordova plugin for execution-queue) 
213 	 *  
214      * @memberOf mmir.env.statemachine.engine.exec#
215 	 */
216     var _androidFactory = {
217 		/** @scope  StateEngineQueuePluginImpl */
218 
219 		/** @memberOf  StateEngineQueuePluginImpl */
220         name: 'queuepluginGen',
221 		createWorker: (function initWorkerFactory() {
222 
223 			//"global" ID-list for all queues (i.e. ID-list for all engines)
224 			var callBackList = [];
225 
226 			return function workerFactory(_instance, gen){
227 
228 				var id = callBackList.length;
229 				var execQueue = window.cordova.plugins.queuePlugin;
230 
231 				function successCallBackHandler(args){
232 					if (args.length=2){
233 //	  					console.debug('QueuePlugin: success '+ JSON.stringify(args[0]) + ', '+JSON.stringify(args[1]));//DEBUG
234 						callBackList[args[0]](args[1]);
235 					}
236 				}
237 
238 				function failureFallbackHandler(err){
239 
240 					_instance._log.error('failed to initialize SCION extension for ANDROID evn');
241 					_instance.worker = (function(gen){
242 						return {
243 							raiseCordova: function fallback(event, eventData){
244 								setTimeout(function(){
245 									gen.call(_instance, event, eventData);
246 								}, 10);
247 							}
248 						};
249 					})();//END: fallback
250 				}
251 
252 				callBackList.push(function(data){
253 					var inst = _instance;
254 					if(inst._log.isv()) inst._log.debug('raising:'+ data.event);
255 					var generatedState = gen.call(inst, data.event, data.eventData);
256 					if(inst._log.isv()) inst._log.debug('QueuePlugin: processed event '+id+' for '+ data.event+' -> new state: '+JSON.stringify(generatedState)+ ' at ' + inst.url);
257 					execQueue.readyForJob(id, successCallBackHandler, failureFallbackHandler);
258 
259 					inst.onraise();
260 				});
261 				execQueue.newQueue(id, function(args){
262 						if(_instance._log.isv()) _instance._log.debug('QueuePlugin: entry '+id+' created.' + ' at ' + _instance.url);
263 					}, failureFallbackHandler
264 				);
265 
266 				return {
267 					_engineInstance: _instance,
268 					raiseCordova: function (event, eventData){
269 						if(this._engineInstance._log.isv()) this._engineInstance._log.debug('QueuePlugin: new Job: '+ id + ' at ' + this._engineInstance.url);
270 						execQueue.newJob(id, {event: event, eventData: eventData}, successCallBackHandler,failureFallbackHandler);
271 					}
272 				};
273 				
274 			};//END: workerFactory(_instance, gen)
275 
276 		})(),//END: initWorkerFactory()
277 		
278 		createRaise: function(_instance){
279 			return function(event, eventData) {
280 
281 				if (eventData) _instance._log.log('new Job:' + event);
282 
283 				_instance.worker.raiseCordova(event, eventData);
284 			};
285 		}
286 
287     };
288 
289     /**
290 	 * Factory for stub/dummy implementation of raise function.
291 	 * 
292 	 * Provide creator-functions:
293 	 * <code>createWorker(_engineInstance, genFunc) : WebWorker</code>
294 	 * <code>createRaise(_engineInstance) : Function</code>
295 	 * 
296 	 * 
297 	 * @requires cordova.plugins.queuePlugin (Cordova plugin for execution-queue) 
298 	 * 
299      * @memberOf mmir.env.statemachine.engine.exec#
300 	 */
301     var _stubFactory = {
302 		/** @scope  StateEngineStubImpl */
303 
304 		/** @memberOf  StateEngineStubImpl */
305         name: 'stubGen',
306 		createWorker: function(_instance, gen) {
307 
308 			return { 
309 				raiseStubImpl: function fallback(event, eventData){
310 					setTimeout(function(){
311 						gen(event, eventData);
312 					}, 50);
313 				}
314 			};
315 
316 		},
317 		createRaise: function(_instance){
318 			return function(event, eventData) {
319 
320 				if (eventData) _instance._log.log('new Job:' + event);
321 
322 				_instance.worker.raiseStubImpl(event, eventData);
323 			};
324 		}
325     };
326 
327     /**
328      * Get factory for raise-impl. depending on the runtime environment.
329      * 
330      * Currently there are 3 impl. available
331      *  * WebWorker
332      *  * Android QueuePlugin
333      *  * STUB IMPLEMENTATION
334      * 
335      * @memberOf mmir.env.statemachine.engine.exec#
336      */
337     function getScionEnvFactory(){
338     	
339     	var hasWebWorkers = typeof window.Worker !== 'undefined';
340     	
341     	//TODO make this configurable? through ConfigurationManager?
342     	if(hasWebWorkers && constants.isBrowserEnv()){
343     		return _browserFactory; //_browser;
344     	}
345     	else {
346     		var isCordovaEnv = !constants.isBrowserEnv();
347         	if(isCordovaEnv){
348         		return _androidFactory;//_cordova;
349         	}
350         	else {
351         		return _stubFactory;//_stub;
352         	}	
353     	}
354     	
355     }
356     
357     /**
358      * no-op function for dummy logger
359      * @memberOf mmir.env.statemachine.engine.exec#
360      */
361     function noop(){};
362     /**
363      * always-false function for dummy logger
364      * @memberOf mmir.env.statemachine.engine.exec#
365      */
366     function deny(){return false;};
367 //    /**
368 //     * print-warning function for dummy logger
369 //     * @memberOf mmir.env.statemachine.engine.exec#
370 //     */
371 //	function pw(){console.warn.apply(console,arguments);};
372     /**
373      * print-error function for dummy logger
374      * @memberOf mmir.env.statemachine.engine.exec#
375      */
376 	function pe(){console.error.apply(console,arguments);};
377 	/**
378      * dummy logger that does nothing:
379      * the engine-creator should replace this with a "real" implementation
380      * e.g. something like this (see also init() in dialogManager):
381      *
382      *  engine = require('engineConfig')('some-url', 'some-mode');
383      *  engine._log = require('logger').create('my-module-id');
384      *  
385      * @memberOf mmir.env.statemachine.engine.exec#
386      */
387     var nolog = {
388         /** @scope mmir.env.statemachine.engine.exec.DummyLogger */
389     	/** @memberOf DummyLogger */
390     	d: noop,
391     	debug: noop,
392     	w: noop,//pw,
393     	warn: noop,//pw,
394     	e: pe,
395     	error: pe,
396     	log: noop,
397     	isVerbose: deny,
398     	isv: deny,
399     	isDebug: deny,
400     	isd: deny
401     };
402     
403     /**
404      * Exported factory function for creating / adding impl. to SCION engine instance
405      *  
406      * @memberOf mmir.env.statemachine.engine.exec#
407      */
408 	return function create(url, _mode) {
409 		
410 		var baseFactory = _defaultFactory;
411 		var scionEnvConfig = getScionEnvFactory();
412 
413 		var _instance = {url: url,_log: nolog};
414 //		var scionConfig = scionEnvConfig(_instance);
415 		var scionConfig = baseFactory( _instance,  scionEnvConfig);
416 		
417 		scionConfig.doc = url;
418 		_instance = createScionEngine(scionConfig, _instance);
419 		
420 		return _instance;
421 	};
422 
423 });
424