1 /**
  2  * Emily.js - http://flams.github.com/emily/
  3  * Copyright(c) 2012-2014 Olivier Scherrer <pode.fr@gmail.com>
  4  * MIT Licensed
  5  */
  6 "use strict";
  7 
  8 var Tools = require("./Tools");
  9 
 10 /**
 11  * @class
 12  * Creates a stateMachine
 13  *
 14  * @param initState {String} the initial state
 15  * @param diagram {Object} the diagram that describes the state machine
 16  * @example
 17  *
 18  *      diagram = {
 19  *              "State1" : [
 20  *                      [ message1, action, nextState], // Same as the state's add function
 21  *                      [ message2, action2, nextState]
 22  *              ],
 23  *
 24  *              "State2" :
 25  *                       [ message3, action3, scope3, nextState]
 26  *                      ... and so on ....
 27  *
 28  *   }
 29  *
 30  * @return the stateMachine object
 31  */
 32 module.exports = function StateMachineConstructor($initState, $diagram) {
 33 
 34     /**
 35      * The list of states
 36      * @private
 37      */
 38     var _states = {},
 39 
 40     /**
 41      * The current state
 42      * @private
 43      */
 44     _currentState = "";
 45 
 46     /**
 47      * Set the initialization state
 48      * @param {String} name the name of the init state
 49      * @returns {Boolean}
 50      */
 51     this.init = function init(name) {
 52             if (_states[name]) {
 53                 _currentState = name;
 54                 return true;
 55             } else {
 56                 return false;
 57             }
 58     };
 59 
 60     /**
 61      * Add a new state
 62      * @private
 63      * @param {String} name the name of the state
 64      * @returns {State} a new state
 65      */
 66     this.add = function add(name) {
 67         if (!_states[name]) {
 68             var transition = _states[name] = new Transition();
 69             return transition;
 70         } else {
 71             return _states[name];
 72         }
 73     };
 74 
 75     /**
 76      * Get an existing state
 77      * @private
 78      * @param {String} name the name of the state
 79      * @returns {State} the state
 80      */
 81     this.get = function get(name) {
 82         return _states[name];
 83     };
 84 
 85     /**
 86      * Get the current state
 87      * @returns {String}
 88      */
 89     this.getCurrent = function getCurrent() {
 90         return _currentState;
 91     };
 92 
 93     /**
 94      * Tell if the state machine has the given state
 95      * @param {String} state the name of the state
 96      * @returns {Boolean} true if it has the given state
 97      */
 98     this.has = function has(state) {
 99         return _states.hasOwnProperty(state);
100     };
101 
102     /**
103      * Advances the state machine to a given state
104      * @param {String} state the name of the state to advance the state machine to
105      * @returns {Boolean} true if it has the given state
106      */
107     this.advance = function advance(state) {
108         if (this.has(state)) {
109             _currentState = state;
110             return true;
111         } else {
112             return false;
113         }
114     };
115 
116     /**
117      * Pass an event to the state machine
118      * @param {String} name the name of the event
119      * @returns {Boolean} true if the event exists in the current state
120      */
121     this.event = function event(name) {
122         var nextState;
123 
124         nextState = _states[_currentState].event.apply(_states[_currentState].event, Tools.toArray(arguments));
125         // False means that there's no such event
126         // But undefined means that the state doesn't change
127         if (nextState === false) {
128             return false;
129         } else {
130             // There could be no next state, so the current one remains
131             if (nextState) {
132                 // Call the exit action if any
133                 _states[_currentState].event("exit");
134                 _currentState = nextState;
135                 // Call the new state's entry action if any
136                 _states[_currentState].event("entry");
137             }
138             return true;
139         }
140     };
141 
142     /**
143      * Initializes the StateMachine with the given diagram
144      */
145     Tools.loop($diagram, function (transition, state) {
146         var myState = this.add(state);
147         transition.forEach(function (params){
148             myState.add.apply(null, params);
149         });
150     }, this);
151 
152     /**
153      * Sets its initial state
154      */
155     this.init($initState);
156 };
157 
158 /**
159  * Each state has associated transitions
160  * @constructor
161  */
162 function Transition() {
163 
164     /**
165      * The list of transitions associated to a state
166      * @private
167      */
168     var _transitions = {};
169 
170     /**
171      * Add a new transition
172      * @private
173      * @param {String} event the event that will trigger the transition
174      * @param {Function} action the function that is executed
175      * @param {Object} scope [optional] the scope in which to execute the action
176      * @param {String} next [optional] the name of the state to transit to.
177      * @returns {Boolean} true if success, false if the transition already exists
178      */
179     this.add = function add(event, action, scope, next) {
180 
181         var arr = [];
182 
183         if (_transitions[event]) {
184             return false;
185         }
186 
187         if (typeof event == "string" &&
188             typeof action == "function") {
189 
190                 arr[0] = action;
191 
192                 if (typeof scope == "object") {
193                     arr[1] = scope;
194                 }
195 
196                 if (typeof scope == "string") {
197                     arr[2] = scope;
198                 }
199 
200                 if (typeof next == "string") {
201                     arr[2] = next;
202                 }
203 
204                 _transitions[event] = arr;
205                 return true;
206         }
207 
208         return false;
209     };
210 
211     /**
212      * Check if a transition can be triggered with given event
213      * @private
214      * @param {String} event the name of the event
215      * @returns {Boolean} true if exists
216      */
217     this.has = function has(event) {
218         return !!_transitions[event];
219     };
220 
221     /**
222      * Get a transition from it's event
223      * @private
224      * @param {String} event the name of the event
225      * @return the transition
226      */
227     this.get = function get(event) {
228         return _transitions[event] || false;
229     };
230 
231     /**
232      * Execute the action associated to the given event
233      * @param {String} event the name of the event
234      * @param {params} params to pass to the action
235      * @private
236      * @returns false if error, the next state or undefined if success (that sounds weird)
237      */
238     this.event = function event(newEvent) {
239         var _transition = _transitions[newEvent];
240         if (_transition) {
241             _transition[0].apply(_transition[1], Tools.toArray(arguments).slice(1));
242             return _transition[2];
243         } else {
244             return false;
245         }
246     };
247 }