1 // ==========================================================================
  2 // Project:   SC.Statechart - A Statechart Framework for SproutCore
  3 // Copyright: ©2010, 2011 Michael Cohen, and contributors.
  4 //            Portions @2011 Apple Inc. All rights reserved.
  5 // License:   Licensed under MIT license (see license.js)
  6 // ==========================================================================
  7 
  8 /*global SC */
  9 
 10 sc_require('system/state');
 11 sc_require('mixins/statechart_delegate');
 12 
 13 /**
 14   @class
 15 
 16   The startchart manager mixin allows an object to be a statechart. By becoming a statechart, the
 17   object can then be manage a set of its own states.
 18 
 19   This implementation of the statechart manager closely follows the concepts stated in D. Harel's
 20   original paper "Statecharts: A Visual Formalism For Complex Systems"
 21   (www.wisdom.weizmann.ac.il/~harel/papers/Statecharts.pdf).
 22 
 23   The statechart allows for complex state heircharies by nesting states within states, and
 24   allows for state orthogonality based on the use of concurrent states.
 25 
 26   At minimum, a statechart must have one state: The root state. All other states in the statechart
 27   are a decendents (substates) of the root state.
 28 
 29   The following example shows how states are nested within a statechart:
 30 
 31       MyApp.Statechart = SC.Object.extend(SC.StatechartManager, {
 32         rootState: SC.State.design({
 33           initialSubstate: 'stateA',
 34 
 35           stateA: SC.State.design({
 36             // ... can continue to nest further states
 37           }),
 38 
 39           stateB: SC.State.design({
 40             // ... can continue to nest further states
 41           })
 42         })
 43       });
 44 
 45   Note how in the example above, the root state as an explicit initial substate to enter into. If no
 46   initial substate is provided, then the statechart will default to the the state's first substate.
 47 
 48   You can also defined states without explicitly defining the root state. To do so, simply create properties
 49   on your object that represents states. Upon initialization, a root state will be constructed automatically
 50   by the mixin and make the states on the object substates of the root state. As an example:
 51 
 52       MyApp.Statechart = SC.Object.extend(SC.StatechartManager, {
 53         initialState: 'stateA',
 54 
 55         stateA: SC.State.design({
 56           // ... can continue to nest further states
 57         }),
 58 
 59         stateB: SC.State.design({
 60           // ... can continue to nest further states
 61         })
 62       });
 63 
 64   If you liked to specify a class that should be used as the root state but using the above method to defined
 65   states, you can set the rootStateExample property with a class that extends from SC.State. If the
 66   rootStateExample property is not explicitly assigned the then default class used will be SC.State.
 67 
 68   To provide your statechart with orthogonality, you use concurrent states. If you use concurrent states,
 69   then your statechart will have multiple current states. That is because each concurrent state represents an
 70   independent state structure from other concurrent states. The following example shows how to provide your
 71   statechart with concurrent states:
 72 
 73       MyApp.Statechart = SC.Object.extend(SC.StatechartManager, {
 74         rootState: SC.State.design({
 75           substatesAreConcurrent: YES,
 76 
 77           stateA: SC.State.design({
 78             // ... can continue to nest further states
 79           }),
 80 
 81           stateB: SC.State.design({
 82             // ... can continue to nest further states
 83           })
 84         })
 85       });
 86 
 87   Above, to indicate that a state's substates are concurrent, you just have to set the substatesAreConcurrent to
 88   YES. Once done, then stateA and stateB will be independent of each other and each will manage their
 89   own current substates. The root state will then have more then one current substate.
 90 
 91   To define concurrent states directly on the object without explicitly defining a root, you can do the
 92   following:
 93 
 94       MyApp.Statechart = SC.Object.extend(SC.StatechartManager, {
 95         statesAreConcurrent: YES,
 96 
 97         stateA: SC.State.design({
 98           // ... can continue to nest further states
 99         }),
100 
101         stateB: SC.State.design({
102           // ... can continue to nest further states
103         })
104       });
105 
106   Remember that a startchart can have a mixture of nested and concurrent states in order for you to
107   create as complex of statecharts that suite your needs. Here is an example of a mixed state structure:
108 
109       MyApp.Statechart = SC.Object.extend(SC.StatechartManager, {
110         rootState: SC.State.design({
111           initialSubstate: 'stateA',
112 
113           stateA: SC.State.design({
114             substatesAreConcurrent: YES,
115 
116             stateM: SC.State.design({ ... })
117             stateN: SC.State.design({ ... })
118             stateO: SC.State.design({ ... })
119           }),
120 
121           stateB: SC.State.design({
122             initialSubstate: 'stateX',
123 
124             stateX: SC.State.design({ ... })
125             stateY: SC.State.design({ ... })
126           })
127         })
128       });
129 
130   Depending on your needs, a statechart can have lots of states, which can become hard to manage all within
131   one file. To modularize your states and make them easier to manage and maintain, you can plug-in states
132   into other states. Let's say we are using the statechart in the last example above, and all the code is
133   within one file. We could update the code and split the logic across two or more files like so:
134 
135       // state_a.js
136 
137       MyApp.StateA = SC.State.extend({
138         substatesAreConcurrent: YES,
139 
140         stateM: SC.State.design({ ... })
141         stateN: SC.State.design({ ... })
142         stateO: SC.State.design({ ... })
143       });
144 
145       // state_b.js
146 
147       MyApp.StateB = SC.State.extend({
148         substatesAreConcurrent: YES,
149 
150         stateM: SC.State.design({ ... })
151         stateN: SC.State.design({ ... })
152         stateO: SC.State.design({ ... })
153       });
154 
155       // statechart.js
156 
157       MyApp.Statechart = SC.Object.extend(SC.StatechartManager, {
158         rootState: SC.State.design({
159           initialSubstate: 'stateA',
160           stateA: SC.State.plugin('MyApp.StateA'),
161           stateB: SC.State.plugin('MyApp.StateB')
162         })
163       });
164 
165   Using state plug-in functionality is optional. If you use the plug-in feature you can break up your statechart
166   into as many files as you see fit.
167 
168   @author Michael Cohen
169 */
170 
171 SC.StatechartManager = /** @scope SC.StatechartManager.prototype */{
172 
173   //@if(debug)
174   /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */
175 
176   /** @private @property */
177   allowStatechartTracing: function () {
178     var key = this.get('statechartTraceKey');
179     return this.get(key);
180   }.property().cacheable(),
181 
182   /** @private */
183   _statechartTraceDidChange: function () {
184     this.notifyPropertyChange('allowStatechartTracing');
185   },
186 
187   /**
188     @property
189 
190     Returns an object containing current detailed information about
191     the statechart. This is primarily used for diagnostic/debugging
192     purposes.
193 
194     Detailed information includes:
195 
196       - current states
197       - state transition information
198       - event handling information
199 
200     NOTE: This is only available in debug mode!
201 
202     @returns {Hash}
203   */
204   details: function () {
205     var details = {
206       'initialized': this.get('statechartIsInitialized')
207     };
208 
209     if (this.get('name')) {
210       details.name = this.get('name');
211     }
212 
213     if (!this.get('statechartIsInitialized')) {
214       return details;
215     }
216 
217     details['current-states'] = [];
218     this.get('currentStates').forEach(function (state) {
219       details['current-states'].push(state.get('fullPath'));
220     });
221 
222     var stateTransition = {
223       active: this.get('gotoStateActive'),
224       suspended: this.get('gotoStateSuspended')
225     };
226 
227     if (this._gotoStateActions) {
228       stateTransition['transition-sequence'] = [];
229       var actions = this._gotoStateActions,
230           actionToStr = function (action) {
231             var actionName = action.action === SC.ENTER_STATE ? "enter" : "exit";
232             return "%@ %@".fmt(actionName, action.state.get('fullPath'));
233           };
234 
235       actions.forEach(function (action) {
236         stateTransition['transition-sequence'].push(actionToStr(action));
237       });
238 
239       stateTransition['current-transition'] = actionToStr(this._currentGotoStateAction);
240     }
241 
242     details['state-transition'] = stateTransition;
243 
244     if (this._stateHandleEventInfo) {
245       var info = this._stateHandleEventInfo;
246       details['handling-event'] = {
247         state: info.state.get('fullPath'),
248         event: info.event,
249         handler: info.handler
250       };
251     } else {
252       details['handling-event'] = false;
253     }
254 
255     return details;
256   }.property(),
257 
258   /**
259     Returns a formatted string of detailed information about this statechart. Useful
260     for diagnostic/debugging purposes.
261 
262     @returns {String}
263 
264     NOTE: This is only available in debug mode!
265 
266     @see #details
267   */
268   toStringWithDetails: function () {
269     var str = "",
270         header = this.toString(),
271         details = this.get('details');
272 
273     str += header + "\n";
274     str += this._hashToString(details, 2);
275 
276     return str;
277   },
278 
279   /** @private */
280   _hashToString: function (hash, indent) {
281     var str = "";
282 
283     for (var key in hash) {
284       var value = hash[key];
285       if (value instanceof Array) {
286         str += this._arrayToString(key, value, indent) + "\n";
287       }
288       else if (value instanceof Object) {
289         str += "%@%@:\n".fmt(' '.mult(indent), key);
290         str += this._hashToString(value, indent + 2);
291       }
292       else {
293         str += "%@%@: %@\n".fmt(' '.mult(indent), key, value);
294       }
295     }
296 
297     return str;
298   },
299 
300   /** @private */
301   _arrayToString: function (key, array, indent) {
302     if (array.length === 0) {
303       return "%@%@: []".fmt(' '.mult(indent), key);
304     }
305 
306     var str = "%@%@: [\n".fmt(' '.mult(indent), key);
307 
308     array.forEach(function (item, idx) {
309       str += "%@%@\n".fmt(' '.mult(indent + 2), item);
310     }, this);
311 
312     str += ' '.mult(indent) + "]";
313 
314     return str;
315   },
316 
317   /**
318     Indicates whether to use a monitor to monitor that statechart's activities. If true then
319     the monitor will be active, otherwise the monitor will not be used. Useful for debugging
320     purposes.
321 
322     NOTE: This is only available in debug mode!
323 
324     @type Boolean
325   */
326   monitorIsActive: NO,
327 
328   /**
329     A statechart monitor that can be used to monitor this statechart. Useful for debugging purposes.
330     A monitor will only be used if monitorIsActive is true.
331 
332     NOTE: This is only available in debug mode!
333 
334     @property {SC.StatechartMonitor}
335   */
336   monitor: null,
337 
338   /**
339     Used to specify what property (key) on the statechart should be used as the trace property. By
340     default the property is 'trace'.
341 
342     NOTE: This is only available in debug mode!
343 
344     @type String
345   */
346   statechartTraceKey: 'trace',
347 
348   /**
349     Indicates whether to trace the statecharts activities. If true then the statechart will output
350     its activites to the browser's JS console. Useful for debugging purposes.
351 
352     NOTE: This is only available in debug mode!
353 
354     @see #statechartTraceKey
355 
356     @type Boolean
357   */
358   trace: NO,
359 
360   /**
361     Used to log a statechart trace message
362 
363     NOTE: This is only available in debug mode!
364   */
365   statechartLogTrace: function (msg, style) {
366     if (style) {
367       SC.Logger.log("%c%@: %@".fmt(this.get('statechartLogPrefix'), msg), style);
368     } else {
369       SC.Logger.info("%@: %@".fmt(this.get('statechartLogPrefix'), msg));
370     }
371   },
372 
373   /* END DEBUG ONLY PROPERTIES AND METHODS */
374   //@endif
375 
376   // Walk like a duck
377   isResponderContext: YES,
378 
379   // Walk like a duck
380   isStatechart: YES,
381 
382   /**
383     Indicates if this statechart has been initialized
384 
385     @type Boolean
386   */
387   statechartIsInitialized: NO,
388 
389   /**
390     Optional name you can provide the statechart with. If set this will be included
391     in tracing and error output as well as detail output. Useful for
392     debugging/diagnostic purposes
393   */
394   name: null,
395 
396   /**
397     The root state of this statechart. All statecharts must have a root state.
398 
399     If this property is left unassigned then when the statechart is initialized
400     it will used the rootStateExample, initialState, and statesAreConcurrent
401     properties to construct a root state.
402 
403     @see #rootStateExample
404     @see #initialState
405     @see #statesAreConcurrent
406 
407     @property {SC.State}
408   */
409   rootState: null,
410 
411   /**
412     Represents the class used to construct a class that will be the root state for
413     this statechart. The class assigned must derive from SC.State.
414 
415     This property will only be used if the rootState property is not assigned.
416 
417     @see #rootState
418 
419     @property {SC.State}
420   */
421   rootStateExample: SC.State,
422 
423   /**
424     Indicates what state should be the initial state of this statechart. The value
425     assigned must be the name of a property on this object that represents a state.
426     As well, the statesAreConcurrent must be set to NO.
427 
428     This property will only be used if the rootState property is not assigned.
429 
430     @see #rootState
431 
432     @type String
433   */
434   initialState: null,
435 
436   /**
437     Indicates if properties on this object representing states are concurrent to each other.
438     If YES then they are concurrent, otherwise they are not. If the YES, then the
439     initialState property must not be assigned.
440 
441     This property will only be used if the rootState property is not assigned.
442 
443     @see #rootState
444 
445     @type Boolean
446   */
447   statesAreConcurrent: NO,
448 
449   /**
450     Used to specify what property (key) on the statechart should be used as the owner property. By
451     default the property is 'owner'.
452 
453     @type String
454   */
455   statechartOwnerKey: 'owner',
456 
457   /**
458     Sets who the owner is of this statechart. If null then the owner is this object otherwise
459     the owner is the assigned object.
460 
461     @see #statechartOwnerKey
462 
463     @type SC.Object
464   */
465   owner: null,
466 
467   /**
468     Indicates if the statechart should be automatically initialized by this
469     object after it has been created. If YES then initStatechart will be
470     called automatically, otherwise it will not.
471 
472     @type Boolean
473   */
474   autoInitStatechart: YES,
475 
476   /**
477     If yes, any warning messages produced by the statechart or any of its states will
478     not be logged, otherwise all warning messages will be logged.
479 
480     While designing and debugging your statechart, it's best to keep this value false.
481     In production you can then suppress the warning messages.
482 
483     @type Boolean
484   */
485   suppressStatechartWarnings: NO,
486 
487   /**
488     A statechart delegate used by the statechart and the states that the statechart
489     manages. The value assigned must adhere to the {@link SC.StatechartDelegate} mixin.
490 
491     @type SC.Object
492 
493     @see SC.StatechartDelegate
494   */
495   delegate: null,
496 
497   /**
498     Computed property that returns an objects that adheres to the
499     {@link SC.StatechartDelegate} mixin. If the {@link #delegate} is not
500     assigned then this object is the default value returned.
501 
502     @see SC.StatechartDelegate
503     @see #delegate
504   */
505   statechartDelegate: function () {
506     var del = this.get('delegate');
507     return this.delegateFor('isStatechartDelegate', del);
508   }.property('delegate'),
509 
510   initMixin: function () {
511     if (this.get('autoInitStatechart')) {
512       this.initStatechart();
513     }
514   },
515 
516   destroyMixin: function () {
517     var root = this.get('rootState');
518 
519     //@if(debug)
520     var traceKey = this.get('statechartTraceKey');
521 
522     this.removeObserver(traceKey, this, '_statechartTraceDidChange');
523     //@endif
524 
525     root.destroy();
526     this.set('rootState', null);
527     this.notifyPropertyChange('currentStates');
528   },
529 
530   /**
531     Initializes the statechart. By initializing the statechart, it will create all the states and register
532     them with the statechart. Once complete, the statechart can be used to go to states and send events to.
533   */
534   initStatechart: function () {
535     if (this.get('statechartIsInitialized')) return;
536 
537     this._gotoStateLocked = NO;
538     this._sendEventLocked = NO;
539     this._pendingStateTransitions = [];
540     this._pendingSentEvents = [];
541 
542     this.sendAction = this.sendEvent;
543 
544     //@if(debug)
545     if (this.get('monitorIsActive')) {
546       this.set('monitor', SC.StatechartMonitor.create({ statechart: this }));
547     }
548 
549     var traceKey = this.get('statechartTraceKey'),
550       trace = this.get('allowStatechartTracing');
551 
552     this.addObserver(traceKey, this, '_statechartTraceDidChange');
553     this._statechartTraceDidChange();
554 
555     if (trace) this.statechartLogTrace("BEGIN initialize statechart", SC.TRACE_STATECHART_STYLE.init);
556     //@endif
557 
558     var rootState = this.get('rootState'),
559         msg;
560 
561     // If no root state was explicitly defined then try to construct
562     // a root state class
563     if (!rootState) {
564       rootState = this._constructRootStateClass();
565     }
566     else if (SC.typeOf(rootState) === SC.T_FUNCTION && rootState.statePlugin) {
567       rootState = rootState.apply(this);
568     }
569 
570     if (!(SC.kindOf(rootState, SC.State) && rootState.isClass)) {
571       msg = "Unable to initialize statechart. Root state must be a state class";
572       this.statechartLogError(msg);
573       SC.throw(msg);
574     }
575 
576     rootState = this.createRootState(rootState, {
577       statechart: this,
578       name: SC.ROOT_STATE_NAME
579     });
580 
581     this.set('rootState', rootState);
582     rootState.initState();
583 
584     if (SC.kindOf(rootState.get('initialSubstate'), SC.EmptyState)) {
585       msg = "Unable to initialize statechart. Root state must have an initial substate explicitly defined";
586       this.statechartLogError(msg);
587       SC.throw(msg);
588     }
589 
590     if (!SC.empty(this.get('initialState'))) {
591       var key = 'initialState';
592       this.set(key, rootState.get(this.get(key)));
593     }
594 
595     this.set('statechartIsInitialized', YES);
596     this.gotoState(rootState);
597 
598     //@if(debug)
599     if (trace) this.statechartLogTrace("END initialize statechart", SC.TRACE_STATECHART_STYLE.init);
600     //@endif
601   },
602 
603   /**
604     Will create a root state for the statechart
605   */
606   createRootState: function (state, attrs) {
607     if (!attrs) attrs = {};
608     state = state.create(attrs);
609     return state;
610   },
611 
612   /**
613     Returns an array of all the current states for this statechart
614 
615     @returns {Array} the current states
616   */
617   currentStates: function () {
618     return this.getPath('rootState.currentSubstates');
619   }.property().cacheable(),
620 
621   /**
622     Returns the first current state for this statechart.
623 
624     @return {SC.State}
625   */
626   firstCurrentState: function () {
627     var cs = this.get('currentStates');
628     return cs ? cs.objectAt(0) : null;
629   }.property('currentStates').cacheable(),
630 
631   /**
632     Returns the count of the current states for this statechart
633 
634     @returns {Number} the count
635   */
636   currentStateCount: function () {
637     return this.getPath('currentStates.length');
638   }.property('currentStates').cacheable(),
639 
640   /**
641     Checks if a given state is a current state of this statechart.
642 
643     @param state {State|String} the state to check
644     @returns {Boolean} true if the state is a current state, otherwise fals is returned
645   */
646   stateIsCurrentState: function (state) {
647     return this.get('rootState').stateIsCurrentSubstate(state);
648   },
649 
650   /**
651     Returns an array of all the states that are currently entered for
652     this statechart.
653 
654     @returns {Array} the currently entered states
655   */
656   enteredStates: function () {
657     return this.getPath('rootState.enteredSubstates');
658   }.property().cacheable(),
659 
660   /**
661     Checks if a given state is a currently entered state of this statechart.
662 
663     @param state {State|String} the state to check
664     @returns {Boolean} true if the state is a currently entered state, otherwise false is returned
665   */
666   stateIsEntered: function (state) {
667     return this.get('rootState').stateIsEnteredSubstate(state);
668   },
669 
670   /**
671     Checks if the given value represents a state is this statechart
672 
673     @param value {State|String} either a state object or the name of a state
674     @returns {Boolean} true if the state does belong ot the statechart, otherwise false is returned
675   */
676   doesContainState: function (value) {
677     return !SC.none(this.getState(value));
678   },
679 
680   /**
681     Gets a state from the statechart that matches the given value
682 
683     @param value {State|String} either a state object of the name of a state
684     @returns {State} if a match then the matching state is returned, otherwise null is returned
685   */
686   getState: function (state) {
687     var root = this.get('rootState');
688     return root === state ? root : root.getSubstate(state);
689   },
690 
691   /**
692     When called, the statechart will proceed with making state transitions in the statechart starting from
693     a current state that meet the statechart conditions. When complete, some or all of the statechart's
694     current states will be changed, and all states that were part of the transition process will either
695     be exited or entered in a specific order.
696 
697     The state that is given to go to will not necessarily be a current state when the state transition process
698     is complete. The final state or states are dependent on factors such an initial substates, concurrent
699     states, and history states.
700 
701     Because the statechart can have one or more current states, it may be necessary to indicate what current state
702     to start from. If no current state to start from is provided, then the statechart will default to using
703     the first current state that it has; depending of the make up of the statechart (no concurrent state vs.
704     with concurrent states), the outcome may be unexpected. For a statechart with concurrent states, it is best
705     to provide a current state in which to start from.
706 
707     When using history states, the statechart will first make transitions to the given state and then use that
708     state's history state and recursively follow each history state's history state until there are no
709     more history states to follow. If the given state does not have a history state, then the statechart
710     will continue following state transition procedures.
711 
712     Method can be called in the following ways:
713 
714         // With one argument.
715         gotoState(<state>)
716 
717         // With two argument.
718         gotoState(<state>, <state | boolean | hash>)
719 
720         // With three argument.
721         gotoState(<state>, <state>, <boolean | hash>)
722         gotoState(<state>, <boolean>, <hash>)
723 
724         // With four argument.
725         gotoState(<state>, <state>, <boolean>, <hash>)
726 
727     where <state> is either a SC.State object or a string and <hash> is a regular JS hash object.
728 
729     @param state {SC.State|String} the state to go to (may not be the final state in the transition process)
730     @param fromCurrentState {SC.State|String} Optional. The current state to start the transition process from.
731     @param useHistory {Boolean} Optional. Indicates whether to include using history states in the transition process
732     @param context {Hash} Optional. A context object that will be passed to all exited and entered states
733   */
734   gotoState: function (state, fromCurrentState, useHistory, context) {
735     if (!this.get('statechartIsInitialized')) {
736       this.statechartLogError("can not go to state %@. statechart has not yet been initialized".fmt(state));
737       return;
738     }
739 
740     if (this.get('isDestroyed')) {
741       this.statechartLogError("can not go to state %@. statechart is destroyed".fmt(this));
742       return;
743     }
744 
745     // Fast arguments access.
746     // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly.
747     var args = new Array(arguments.length); //  SC.$A(arguments)
748     for (var i = 0, len = args.length; i < len; i++) { args[i] = arguments[i]; }
749 
750     args = this._processGotoStateArgs(args);
751 
752     state = args.state;
753     fromCurrentState = args.fromCurrentState;
754     useHistory = args.useHistory;
755     context = args.context;
756 
757     var pivotState = null,
758         exitStates = [],
759         enterStates = [],
760         paramState = state,
761         paramFromCurrentState = fromCurrentState,
762         msg;
763 
764     state = this.getState(state);
765 
766     if (SC.none(state)) {
767       this.statechartLogError("Can not to goto state %@. Not a recognized state in statechart".fmt(paramState));
768       return;
769     }
770 
771     if (this._gotoStateLocked) {
772       // There is a state transition currently happening. Add this requested state
773       // transition to the queue of pending state transitions. The request will
774       // be invoked after the current state transition is finished.
775       this._pendingStateTransitions.push({
776         state: state,
777         fromCurrentState: fromCurrentState,
778         useHistory: useHistory,
779         context: context
780       });
781 
782       return;
783     }
784 
785     // Lock the current state transition so that no other requested state transition
786     // interferes.
787     this._gotoStateLocked = YES;
788 
789     if (fromCurrentState) {
790       // Check to make sure the current state given is actually a current state of this statechart
791       fromCurrentState = this.getState(fromCurrentState);
792       if (SC.none(fromCurrentState) || !fromCurrentState.get('isCurrentState')) {
793         msg = "Can not to goto state %@. %@ is not a recognized current state in statechart";
794         this.statechartLogError(msg.fmt(paramState, paramFromCurrentState));
795         this._gotoStateLocked = NO;
796         return;
797       }
798     }
799     else {
800       // No explicit current state to start from; therefore, need to find a current state
801       // to transition from.
802       fromCurrentState = state.findFirstRelativeCurrentState();
803       if (!fromCurrentState) fromCurrentState = this.get('firstCurrentState');
804     }
805 
806     //@if(debug)
807     var trace = this.get('allowStatechartTracing');
808     if (trace) {
809       this.statechartLogTrace("BEGIN gotoState: %@".fmt(state), SC.TRACE_STATECHART_STYLE.gotoState);
810       msg = "  starting from current state: %@";
811       msg = msg.fmt(fromCurrentState ? fromCurrentState : '-- none --');
812       this.statechartLogTrace(msg, SC.TRACE_STATECHART_STYLE.gotoStateInfo);
813 
814       var len = this.getPath('currentStates.length');
815       // For many states, we list each on its own line.
816       if (len > 2) {
817         msg = "current states before:\n%@";
818         msg = msg.fmt(this.get('currentStates').getEach('fullPath').join('\n'));
819       }
820       // For a few states, all on one line.
821       else if (len > 0) {
822         msg = "  current states before: %@";
823         msg = msg.fmt(this.get('currentStates').getEach('fullPath').join(', '));
824       }
825       // For no states, no states.
826       else {
827         msg = "  current states before: --none--";
828       }
829       this.statechartLogTrace(msg, SC.TRACE_STATECHART_STYLE.gotoStateInfo);
830     }
831     //@endif
832 
833     // If there is a current state to start the transition process from, then determine what
834     // states are to be exited
835     if (!SC.none(fromCurrentState)) {
836       exitStates = this._createStateChain(fromCurrentState);
837     }
838 
839     // Now determine the initial states to be entered
840     enterStates = this._createStateChain(state);
841 
842     // Get the pivot state to indicate when to go from exiting states to entering states
843     pivotState = this._findPivotState(exitStates, enterStates);
844 
845     if (pivotState) {
846       //@if(debug)
847       if (trace) this.statechartLogTrace("  pivot state = %@".fmt(pivotState), SC.TRACE_STATECHART_STYLE.gotoStateInfo);
848       //@endif
849       if (pivotState.get('substatesAreConcurrent') && pivotState !== state) {
850         this.statechartLogError("Can not go to state %@ from %@. Pivot state %@ has concurrent substates.".fmt(state, fromCurrentState, pivotState));
851         this._gotoStateLocked = NO;
852         return;
853       }
854     }
855 
856     // Collect what actions to perform for the state transition process
857     var gotoStateActions = [];
858 
859 
860     // Go ahead and find states that are to be exited
861     this._traverseStatesToExit(exitStates.shift(), exitStates, pivotState, gotoStateActions);
862 
863     // Now go find states that are to be entered
864     if (pivotState !== state) {
865       this._traverseStatesToEnter(enterStates.pop(), enterStates, pivotState, useHistory, gotoStateActions);
866     } else {
867       this._traverseStatesToExit(pivotState, [], null, gotoStateActions);
868       this._traverseStatesToEnter(pivotState, null, null, useHistory, gotoStateActions);
869     }
870 
871     // Collected all the state transition actions to be performed. Now execute them.
872     this._gotoStateActions = gotoStateActions;
873     this._executeGotoStateActions(state, gotoStateActions, null, context);
874   },
875 
876   /**
877     Indicates if the statechart is in an active goto state process
878   */
879   gotoStateActive: function () {
880     return this._gotoStateLocked;
881   }.property(),
882 
883   /**
884     Indicates if the statechart is in an active goto state process
885     that has been suspended
886   */
887   gotoStateSuspended: function () {
888     return this._gotoStateLocked && !!this._gotoStateSuspendedPoint;
889   }.property(),
890 
891   /**
892     Resumes an active goto state transition process that has been suspended.
893   */
894   resumeGotoState: function () {
895     if (!this.get('gotoStateSuspended')) {
896       this.statechartLogError("Can not resume goto state since it has not been suspended");
897       return;
898     }
899 
900     var point = this._gotoStateSuspendedPoint;
901     this._executeGotoStateActions(point.gotoState, point.actions, point.marker, point.context);
902   },
903 
904   /** @private */
905   _executeGotoStateActions: function (gotoState, actions, marker, context) {
906     var action = null,
907         len = actions.length,
908         actionResult = null;
909 
910     marker = SC.none(marker) ? 0 : marker;
911 
912     for (; marker < len; marker += 1) {
913       this._currentGotoStateAction = action = actions[marker];
914       switch (action.action) {
915       case SC.EXIT_STATE:
916         actionResult = this._exitState(action.state, context);
917         break;
918 
919       case SC.ENTER_STATE:
920         actionResult = this._enterState(action.state, action.currentState, context);
921         break;
922       }
923 
924       //
925       // Check if the state wants to perform an asynchronous action during
926       // the state transition process. If so, then we need to first
927       // suspend the state transition process and then invoke the
928       // asynchronous action. Once called, it is then up to the state or something
929       // else to resume this statechart's state transition process by calling the
930       // statechart's resumeGotoState method.
931       //
932       if (SC.kindOf(actionResult, SC.Async)) {
933         this._gotoStateSuspendedPoint = {
934           gotoState: gotoState,
935           actions: actions,
936           marker: marker + 1,
937           context: context
938         };
939 
940         actionResult.tryToPerform(action.state);
941         return;
942       }
943     }
944 
945     this.beginPropertyChanges();
946     this.notifyPropertyChange('currentStates');
947     this.notifyPropertyChange('enteredStates');
948     this.endPropertyChanges();
949 
950     //@if(debug)
951     if (this.get('allowStatechartTracing')) {
952       if (this.getPath('currentStates.length') > 2) {
953         this.statechartLogTrace("  current states after:\n%@".fmt(this.get('currentStates').getEach('fullPath').join('  \n')), SC.TRACE_STATECHART_STYLE.gotoStateInfo);
954       } else {
955         this.statechartLogTrace("  current states after: %@".fmt(this.get('currentStates').getEach('fullPath').join(', ')), SC.TRACE_STATECHART_STYLE.gotoStateInfo);
956       }
957       this.statechartLogTrace("END gotoState: %@".fmt(gotoState), SC.TRACE_STATECHART_STYLE.gotoState);
958     }
959     //@endif
960 
961     this._cleanupStateTransition();
962   },
963 
964   /** @private */
965   _cleanupStateTransition: function () {
966     this._currentGotoStateAction = null;
967     this._gotoStateSuspendedPoint = null;
968     this._gotoStateActions = null;
969     this._gotoStateLocked = NO;
970     this._flushPendingStateTransition();
971     // Check the flags so we only flush if the events will actually get sent.
972     if (!this._sendEventLocked && !this._gotoStateLocked) { this._flushPendingSentEvents(); }
973   },
974 
975   /** @private */
976   _exitState: function (state, context) {
977     var parentState;
978 
979     if (state.get('currentSubstates').indexOf(state) >= 0) {
980       parentState = state.get('parentState');
981       while (parentState) {
982         parentState.get('currentSubstates').removeObject(state);
983         parentState.notifyPropertyChange('currentSubstates');
984         parentState = parentState.get('parentState');
985       }
986     }
987 
988     parentState = state;
989     while (parentState) {
990       parentState.get('enteredSubstates').removeObject(state);
991       parentState.notifyPropertyChange('enteredSubstates');
992       parentState = parentState.get('parentState');
993     }
994 
995     //@if(debug)
996     if (this.get('allowStatechartTracing')) {
997       this.statechartLogTrace("<-- exiting state: %@".fmt(state), SC.TRACE_STATECHART_STYLE.exit);
998     }
999     //@endif
1000 
1001     state.set('currentSubstates', []);
1002     state.notifyPropertyChange('currentSubstates');
1003 
1004     state.stateWillBecomeExited(context);
1005     var result = this.exitState(state, context);
1006     state.stateDidBecomeExited(context);
1007 
1008     //@if(debug)
1009     if (this.get('monitorIsActive')) this.get('monitor').pushExitedState(state);
1010     //@endif
1011 
1012     state._traverseStatesToExit_skipState = NO;
1013 
1014     return result;
1015   },
1016 
1017   /**
1018     What will actually invoke a state's exitState method.
1019 
1020     Called during the state transition process whenever the gotoState method is
1021     invoked.
1022 
1023     @param state {SC.State} the state whose enterState method is to be invoked
1024     @param context {Hash} a context hash object to provide the enterState method
1025   */
1026   exitState: function (state, context) {
1027     return state.exitState(context);
1028   },
1029 
1030   /** @private */
1031   _enterState: function (state, current, context) {
1032     var parentState = state.get('parentState');
1033     if (parentState && !state.get('isConcurrentState')) parentState.set('historyState', state);
1034 
1035     if (current) {
1036       parentState = state;
1037       while (parentState) {
1038         parentState.get('currentSubstates').pushObject(state);
1039         parentState.notifyPropertyChange('currentSubstates');
1040         parentState = parentState.get('parentState');
1041       }
1042     }
1043 
1044     parentState = state;
1045     while (parentState) {
1046       parentState.get('enteredSubstates').pushObject(state);
1047       parentState.notifyPropertyChange('enteredSubstates');
1048       parentState = parentState.get('parentState');
1049     }
1050 
1051     //@if(debug)
1052     if (this.get('allowStatechartTracing')) {
1053       if (state.enterStateByRoute && SC.kindOf(context, SC.StateRouteHandlerContext)) {
1054         this.statechartLogTrace("--> entering state (by route): %@".fmt(state), SC.TRACE_STATECHART_STYLE.enter);
1055       } else {
1056         this.statechartLogTrace("--> entering state: %@".fmt(state), SC.TRACE_STATECHART_STYLE.enter);
1057       }
1058     }
1059     //@endif
1060 
1061     state.stateWillBecomeEntered(context);
1062     var result = this.enterState(state, context);
1063     state.stateDidBecomeEntered(context);
1064 
1065     //@if(debug)
1066     if (this.get('monitorIsActive')) this.get('monitor').pushEnteredState(state);
1067     //@endif
1068 
1069     return result;
1070   },
1071 
1072   /**
1073     What will actually invoke a state's enterState method.
1074 
1075     Called during the state transition process whenever the gotoState method is
1076     invoked.
1077 
1078     If the context provided is a state route context object
1079     ({@link SC.StateRouteContext}), then if the given state has a enterStateByRoute
1080     method, that method will be invoked, otherwise the state's enterState method
1081     will be invoked by default. The state route context object will be supplied to
1082     both enter methods in either case.
1083 
1084     @param state {SC.State} the state whose enterState method is to be invoked
1085     @param context {Hash} a context hash object to provide the enterState method
1086   */
1087   enterState: function (state, context) {
1088     if (state.enterStateByRoute && SC.kindOf(context, SC.StateRouteHandlerContext)) {
1089       return state.enterStateByRoute(context);
1090     } else {
1091       return state.enterState(context);
1092     }
1093   },
1094 
1095   /**
1096     When called, the statechart will proceed to make transitions to the given state then follow that
1097     state's history state.
1098 
1099     You can either go to a given state's history recursively or non-recursively. To go to a state's history
1100     recursively means to following each history state's history state until no more history states can be
1101     followed. Non-recursively means to just to the given state's history state but do not recusively follow
1102     history states. If the given state does not have a history state, then the statechart will just follow
1103     normal procedures when making state transitions.
1104 
1105     Because a statechart can have one or more current states, depending on if the statechart has any concurrent
1106     states, it is optional to provided current state in which to start the state transition process from. If no
1107     current state is provided, then the statechart will default to the first current state that it has; which,
1108     depending on the make up of that statechart, can lead to unexpected outcomes. For a statechart with concurrent
1109     states, it is best to explicitly supply a current state.
1110 
1111     Method can be called in the following ways:
1112 
1113         // With one arguments.
1114         gotoHistoryState(<state>)
1115 
1116         // With two arguments.
1117         gotoHistoryState(<state>, <state | boolean | hash>)
1118 
1119         // With three arguments.
1120         gotoHistoryState(<state>, <state>, <boolean | hash>)
1121         gotoHistoryState(<state>, <boolean>, <hash>)
1122 
1123         // With four argumetns
1124         gotoHistoryState(<state>, <state>, <boolean>, <hash>)
1125 
1126     where <state> is either a SC.State object or a string and <hash> is a regular JS hash object.
1127 
1128     @param state {SC.State|String} the state to go to and follow it's history state
1129     @param fromCurrentState {SC.State|String} Optional. the current state to start the state transition process from
1130     @param recursive {Boolean} Optional. whether to follow history states recursively.
1131   */
1132   gotoHistoryState: function (state, fromCurrentState, recursive, context) {
1133     if (!this.get('statechartIsInitialized')) {
1134       this.statechartLogError("can not go to state %@'s history state. Statechart has not yet been initialized".fmt(state));
1135       return;
1136     }
1137 
1138     // Fast arguments access.
1139     // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly.
1140     var args = new Array(arguments.length); //  SC.$A(arguments)
1141     for (var i = 0, len = args.length; i < len; i++) { args[i] = arguments[i]; }
1142 
1143     args = this._processGotoStateArgs(args);
1144 
1145     state = args.state;
1146     fromCurrentState = args.fromCurrentState;
1147     recursive = args.useHistory;
1148     context = args.context;
1149 
1150     state = this.getState(state);
1151 
1152     if (!state) {
1153       this.statechartLogError("Can not to goto state %@'s history state. Not a recognized state in statechart".fmt(state));
1154       return;
1155     }
1156 
1157     var historyState = state.get('historyState');
1158 
1159     if (!recursive) {
1160       if (historyState) {
1161         this.gotoState(historyState, fromCurrentState, context);
1162       } else {
1163         this.gotoState(state, fromCurrentState, context);
1164       }
1165     } else {
1166       this.gotoState(state, fromCurrentState, YES, context);
1167     }
1168   },
1169 
1170   /**
1171     Sends a given event to all the statechart's current states.
1172 
1173     If a current state does can not respond to the sent event, then the current state's parent state
1174     will be tried. This process is recursively done until no more parent state can be tried.
1175 
1176     Note that a state will only be checked once if it can respond to an event. Therefore, if
1177     there is a state S that handles event foo and S has concurrent substates, then foo will
1178     only be invoked once; not as many times as there are substates.
1179 
1180     @param event {String} name of the event
1181     @param arg1 {Object} optional argument
1182     @param arg2 {Object} optional argument
1183     @returns {SC.Responder} the responder that handled it or null
1184 
1185     @see #stateWillTryToHandleEvent
1186     @see #stateDidTryToHandleEvent
1187   */
1188   sendEvent: function (event, arg1, arg2) {
1189 
1190     if (this.get('isDestroyed')) {
1191       this.statechartLogError("can not send event %@. statechart is destroyed".fmt(event));
1192       return;
1193     }
1194 
1195     var statechartHandledEvent = NO,
1196         result = this,
1197         eventHandled = NO,
1198         currentStates = this.get('currentStates').slice(),
1199         checkedStates = {},
1200         len = 0,
1201         i = 0,
1202         state = null;
1203 
1204     if (this._sendEventLocked || this._gotoStateLocked) {
1205       // Want to prevent any actions from being processed by the states until
1206       // they have had a chance to handle the most immediate action or completed
1207       // a state transition
1208       this._pendingSentEvents.push({
1209         event: event,
1210         arg1: arg1,
1211         arg2: arg2
1212       });
1213 
1214       return;
1215     }
1216 
1217     this._sendEventLocked = YES;
1218 
1219     //@if(debug)
1220     var trace = this.get('allowStatechartTracing');
1221     if (trace) {
1222       this.statechartLogTrace("BEGIN sendEvent: '%@'".fmt(event), SC.TRACE_STATECHART_STYLE.action);
1223     }
1224     //@endif
1225 
1226     len = currentStates.get('length');
1227     for (; i < len; i += 1) {
1228       eventHandled = NO;
1229       state = currentStates[i];
1230       if (!state.get('isCurrentState')) continue;
1231       while (!eventHandled && state) {
1232         if (!checkedStates[state.get('fullPath')]) {
1233           eventHandled = state.tryToHandleEvent(event, arg1, arg2);
1234           checkedStates[state.get('fullPath')] = YES;
1235         }
1236         if (!eventHandled) state = state.get('parentState');
1237         else statechartHandledEvent = YES;
1238       }
1239     }
1240 
1241     // Now that all the states have had a chance to process the
1242     // first event, we can go ahead and flush any pending sent events.
1243     this._sendEventLocked = NO;
1244 
1245     //@if(debug)
1246     if (trace) {
1247       if (!statechartHandledEvent) this.statechartLogTrace("No state was able to handle event %@".fmt(event), SC.TRACE_STATECHART_STYLE.action);
1248       this.statechartLogTrace("END sendEvent: '%@'".fmt(event), SC.TRACE_STATECHART_STYLE.action);
1249     }
1250     //@endif
1251 
1252     // Check if the flags are unlocked. These means any pending events
1253     // will successfully send, so go ahead and flush. Otherwise, events
1254     // would become out of order since the first event would get shifted,
1255     // then pushed.
1256     if (!this._sendEventLocked && !this._gotoStateLocked) {
1257       result = this._flushPendingSentEvents();
1258     }
1259 
1260     return statechartHandledEvent ? this : (result ? this : null);
1261   },
1262 
1263   /**
1264     Used to notify the statechart that a state will try to handle event that has been passed
1265     to it.
1266 
1267     @param {SC.State} state the state that will try to handle the event
1268     @param {String} event the event the state will try to handle
1269     @param {String} handler the name of the method on the state that will try to handle the event
1270   */
1271   stateWillTryToHandleEvent: function (state, event, handler) {
1272     this._stateHandleEventInfo = {
1273       state: state,
1274       event: event,
1275       handler: handler
1276     };
1277   },
1278 
1279   /**
1280     Used to notify the statechart that a state did try to handle event that has been passed
1281     to it.
1282 
1283     @param {SC.State} state the state that did try to handle the event
1284     @param {String} event the event the state did try to handle
1285     @param {String} handler the name of the method on the state that did try to handle the event
1286     @param {Boolean} handled indicates if the handler was able to handle the event
1287   */
1288   stateDidTryToHandleEvent: function (state, event, handler, handled) {
1289     this._stateHandleEventInfo = null;
1290   },
1291 
1292   /** @private
1293 
1294     Creates a chain of states from the given state to the greatest ancestor state (the root state). Used
1295     when perform state transitions.
1296   */
1297   _createStateChain: function (state) {
1298     var chain = [];
1299 
1300     while (state) {
1301       chain.push(state);
1302       state = state.get('parentState');
1303     }
1304 
1305     return chain;
1306   },
1307 
1308   /** @private
1309 
1310     Finds a pivot state from two given state chains. The pivot state is the state indicating when states
1311     go from being exited to states being entered during the state transition process. The value
1312     returned is the fist matching state between the two given state chains.
1313   */
1314   _findPivotState: function (stateChain1, stateChain2) {
1315     if (stateChain1.length === 0 || stateChain2.length === 0) return null;
1316 
1317     var pivot = stateChain1.find(function (state, index) {
1318       if (stateChain2.indexOf(state) >= 0) return YES;
1319     });
1320 
1321     return pivot;
1322   },
1323 
1324   /** @private
1325 
1326     Recursively follow states that are to be exited during a state transition process. The exit
1327     process is to start from the given state and work its way up to when either all exit
1328     states have been reached based on a given exit path or when a stop state has been reached.
1329 
1330     @param state {State} the state to be exited
1331     @param exitStatePath {Array} an array representing a path of states that are to be exited
1332     @param stopState {State} an explicit state in which to stop the exiting process
1333   */
1334   _traverseStatesToExit: function (state, exitStatePath, stopState, gotoStateActions) {
1335     if (!state || state === stopState) return;
1336 
1337     // This state has concurrent substates. Therefore we have to make sure we
1338     // exit them up to this state before we can go any further up the exit chain.
1339     if (state.get('substatesAreConcurrent')) {
1340       var i = 0,
1341           currentSubstates = state.get('currentSubstates'),
1342           len = currentSubstates.length,
1343           currentState = null;
1344 
1345       for (; i < len; i += 1) {
1346         currentState = currentSubstates[i];
1347         if (currentState._traverseStatesToExit_skipState === YES) continue;
1348         var chain = this._createStateChain(currentState);
1349         this._traverseStatesToExit(chain.shift(), chain, state, gotoStateActions);
1350       }
1351     }
1352 
1353     gotoStateActions.push({ action: SC.EXIT_STATE, state: state });
1354     if (state.get('isCurrentState')) state._traverseStatesToExit_skipState = YES;
1355     this._traverseStatesToExit(exitStatePath.shift(), exitStatePath, stopState, gotoStateActions);
1356   },
1357 
1358   /** @private
1359 
1360     Recursively follow states that are to be entered during the state transition process. The
1361     enter process is to start from the given state and work its way down a given enter path. When
1362     the end of enter path has been reached, then continue entering states based on whether
1363     an initial substate is defined, there are concurrent substates or history states are to be
1364     followed; when none of those condition are met then the enter process is done.
1365 
1366     @param state {State} the sate to be entered
1367     @param enterStatePath {Array} an array representing an initial path of states that are to be entered
1368     @param pivotState {State} The state pivoting when to go from exiting states to entering states
1369     @param useHistory {Boolean} indicates whether to recursively follow history states
1370   */
1371   _traverseStatesToEnter: function (state, enterStatePath, pivotState, useHistory, gotoStateActions) {
1372     if (!state) return;
1373 
1374     // We do not want to enter states in the enter path until the pivot state has been reached. After
1375     // the pivot state has been reached, then we can go ahead and actually enter states.
1376     if (pivotState) {
1377       if (state !== pivotState) {
1378         this._traverseStatesToEnter(enterStatePath.pop(), enterStatePath, pivotState, useHistory, gotoStateActions);
1379       } else {
1380         this._traverseStatesToEnter(enterStatePath.pop(), enterStatePath, null, useHistory, gotoStateActions);
1381       }
1382     }
1383 
1384     // If no more explicit enter path instructions, then default to enter states based on
1385     // other criteria
1386     else if (!enterStatePath || enterStatePath.length === 0) {
1387       var gotoStateAction = { action: SC.ENTER_STATE, state: state, currentState: NO };
1388       gotoStateActions.push(gotoStateAction);
1389 
1390       var initialSubstate = state.get('initialSubstate'),
1391           historyState = state.get('historyState');
1392 
1393       // State has concurrent substates. Need to enter all of the substates
1394       if (state.get('substatesAreConcurrent')) {
1395         this._traverseConcurrentStatesToEnter(state.get('substates'), null, useHistory, gotoStateActions);
1396       }
1397 
1398       // State has substates and we are instructed to recursively follow the state's
1399       // history state if it has one.
1400       else if (state.get('hasSubstates') && historyState && useHistory) {
1401         this._traverseStatesToEnter(historyState, null, null, useHistory, gotoStateActions);
1402       }
1403 
1404       // State has an initial substate to enter
1405       else if (initialSubstate) {
1406         if (SC.kindOf(initialSubstate, SC.HistoryState)) {
1407           if (!useHistory) useHistory = initialSubstate.get('isRecursive');
1408           initialSubstate = initialSubstate.get('state');
1409         }
1410         this._traverseStatesToEnter(initialSubstate, null, null, useHistory, gotoStateActions);
1411       }
1412 
1413       // Looks like we hit the end of the road. Therefore the state has now become
1414       // a current state of the statechart.
1415       else {
1416         gotoStateAction.currentState = YES;
1417       }
1418     }
1419 
1420     // Still have an explicit enter path to follow, so keep moving through the path.
1421     else if (enterStatePath.length > 0) {
1422       gotoStateActions.push({ action: SC.ENTER_STATE, state: state });
1423       var nextState = enterStatePath.pop();
1424       this._traverseStatesToEnter(nextState, enterStatePath, null, useHistory, gotoStateActions);
1425 
1426       // We hit a state that has concurrent substates. Must go through each of the substates
1427       // and enter them
1428       if (state.get('substatesAreConcurrent')) {
1429         this._traverseConcurrentStatesToEnter(state.get('substates'), nextState, useHistory, gotoStateActions);
1430       }
1431     }
1432   },
1433 
1434   /** @override
1435 
1436     Returns YES if the named value translates into an executable function on
1437     any of the statechart's current states or the statechart itself.
1438 
1439     @param event {String} the property name to check
1440     @returns {Boolean}
1441   */
1442   respondsTo: function (event) {
1443     // Fast path!
1444     if (this.get('isDestroyed')) {
1445       this.statechartLogError("can not respond to event %@. statechart is destroyed".fmt(event));
1446       return false;
1447     }
1448 
1449     var currentStates = this.get('currentStates'),
1450         len = currentStates.get('length'),
1451         i = 0, state = null;
1452 
1453     for (; i < len; i += 1) {
1454       state = currentStates.objectAt(i);
1455       while (state) {
1456         if (state.respondsToEvent(event)) return true;
1457         state = state.get('parentState');
1458       }
1459     }
1460 
1461     // None of the current states can respond. Now check the statechart itself
1462     return SC.typeOf(this[event]) === SC.T_FUNCTION;
1463   },
1464 
1465   /** @override
1466 
1467     Attempts to handle a given event against any of the statechart's current states and the
1468     statechart itself. If any current state can handle the event or the statechart itself can
1469     handle the event then YES is returned, otherwise NO is returned.
1470 
1471     @param event {String} what to perform
1472     @param arg1 {Object} Optional
1473     @param arg2 {Object} Optional
1474     @returns {Boolean} YES if handled, NO if not handled
1475   */
1476   tryToPerform: function (event, arg1, arg2) {
1477     if (!this.respondsTo(event)) return NO;
1478 
1479     if (SC.typeOf(this[event]) === SC.T_FUNCTION) {
1480       var result = this[event](arg1, arg2);
1481       if (result !== NO) return YES;
1482     }
1483 
1484     return !!this.sendEvent(event, arg1, arg2);
1485   },
1486 
1487   /**
1488     Used to invoke a method on current states. If the method can not be executed
1489     on a current state, then the state's parent states will be tried in order
1490     of closest ancestry.
1491 
1492     A few notes:
1493 
1494      1. Calling this is not the same as calling sendEvent or sendAction.
1495         Rather, this should be seen as calling normal methods on a state that
1496         will *not* call gotoState or gotoHistoryState.
1497      2. A state will only ever be invoked once per call. So if there are two
1498         or more current states that have the same parent state, then that parent
1499         state will only be invoked once if none of the current states are able
1500         to invoke the given method.
1501 
1502     When calling this method, you are able to supply zero ore more arguments
1503     that can be pass onto the method called on the states. As an example
1504 
1505         invokeStateMethod('render', context, firstTime);
1506 
1507     The above call will invoke the render method on the current states
1508     and supply the context and firstTime arguments to the method.
1509 
1510     Because a statechart can have more than one current state and the method
1511     invoked may return a value, the addition of a callback function may be provided
1512     in order to handle the returned value for each state. As an example, let's say
1513     we want to call a calculate method on the current states where the method
1514     will return a value when invoked. We can handle the returned values like so:
1515 
1516         invokeStateMethod('calculate', value, function (state, result) {
1517           // .. handle the result returned from calculate that was invoked
1518           //    on the given state
1519         })
1520 
1521     If the method invoked does not return a value and a callback function is
1522     supplied, then result value will simply be undefined. In all cases, if
1523     a callback function is given, it must be the last value supplied to this
1524     method.
1525 
1526     invokeStateMethod will return a value if only one state was able to have
1527     the given method invoked on it, otherwise no value is returned.
1528 
1529     @param methodName {String} methodName a method name
1530     @param args {Object...} Optional. any additional arguments
1531     @param func {Function} Optional. a callback function. Must be the last
1532            value supplied if provided.
1533 
1534     @returns a value if the number of current states is one, otherwise undefined
1535              is returned. The value is the result of the method that got invoked
1536              on a state.
1537   */
1538   invokeStateMethod: function (methodName, args, func) {
1539     if (methodName === 'unknownEvent') {
1540       this.statechartLogError("can not invoke method unkownEvent");
1541       return;
1542     }
1543 
1544     args = SC.A(arguments);
1545     args.shift();
1546 
1547     var len = args.length,
1548         arg = len > 0 ? args[len - 1] : null,
1549         callback = SC.typeOf(arg) === SC.T_FUNCTION ? args.pop() : null,
1550         currentStates = this.get('currentStates'),
1551         i = 0, state = null,
1552         checkedStates = {},
1553         method, result,
1554         calledStates = 0;
1555 
1556     len = currentStates.get('length');
1557 
1558     for (; i < len; i += 1) {
1559       state = currentStates.objectAt(i);
1560       while (state) {
1561         if (checkedStates[state.get('fullPath')]) break;
1562         checkedStates[state.get('fullPath')] = YES;
1563         method = state[methodName];
1564         if (SC.typeOf(method) === SC.T_FUNCTION && !method.isEventHandler) {
1565           result = method.apply(state, args);
1566           if (callback) callback.call(this, state, result);
1567           calledStates += 1;
1568           break;
1569         }
1570         state = state.get('parentState');
1571       }
1572     }
1573 
1574     return calledStates === 1 ? result : undefined;
1575   },
1576 
1577   /** @private
1578 
1579     Iterate over all the given concurrent states and enter them
1580   */
1581   _traverseConcurrentStatesToEnter: function (states, exclude, useHistory, gotoStateActions) {
1582     var i = 0,
1583         len = states.length,
1584         state = null;
1585 
1586     for (; i < len; i += 1) {
1587       state = states[i];
1588       if (state !== exclude) this._traverseStatesToEnter(state, null, null, useHistory, gotoStateActions);
1589     }
1590   },
1591 
1592   /** @private
1593 
1594     Called by gotoState to flush a pending state transition at the front of the
1595     pending queue.
1596   */
1597   _flushPendingStateTransition: function () {
1598     if (!this._pendingStateTransitions) {
1599       this.statechartLogError("Unable to flush pending state transition. _pendingStateTransitions is invalid");
1600       return;
1601     }
1602     var pending = this._pendingStateTransitions.shift();
1603     if (!pending) return;
1604     this.gotoState(pending.state, pending.fromCurrentState, pending.useHistory, pending.context);
1605   },
1606 
1607   /** @private
1608 
1609      Called by sendEvent to flush a pending actions at the front of the pending
1610      queue
1611    */
1612   _flushPendingSentEvents: function () {
1613     var pending = this._pendingSentEvents.shift();
1614     if (!pending) return null;
1615     return this.sendEvent(pending.event, pending.arg1, pending.arg2);
1616   },
1617 
1618   /** @private */
1619   //@if(debug)
1620   _monitorIsActiveDidChange: function () {
1621     if (this.get('monitorIsActive') && SC.none(this.get('monitor'))) {
1622       this.set('monitor', SC.StatechartMonitor.create());
1623     }
1624   }.observes('monitorIsActive'),
1625   //@endif
1626 
1627   /** @private
1628     Will process the arguments supplied to the gotoState method.
1629 
1630     TODO: Come back to this and refactor the code. It works, but it
1631           could certainly be improved
1632   */
1633   _processGotoStateArgs: function (args) {
1634     var processedArgs = {
1635         state: null,
1636         fromCurrentState: null,
1637         useHistory: false,
1638         context: null
1639       },
1640       len = null,
1641       value = null;
1642 
1643     args = args.filter(function (item) {
1644       return item !== undefined;
1645     });
1646     len = args.length;
1647 
1648     if (len < 1) return processedArgs;
1649 
1650     processedArgs.state = args[0];
1651 
1652     if (len === 2) {
1653       value = args[1];
1654       switch (SC.typeOf(value)) {
1655       case SC.T_BOOL:
1656         processedArgs.useHistory = value;
1657         break;
1658       case SC.T_HASH:
1659       case SC.T_OBJECT:
1660         if (!SC.kindOf(value, SC.State)) {
1661           processedArgs.context = value;
1662         }
1663         break;
1664       default:
1665         processedArgs.fromCurrentState = value;
1666       }
1667     }
1668     else if (len === 3) {
1669       value = args[1];
1670       if (SC.typeOf(value) === SC.T_BOOL) {
1671         processedArgs.useHistory = value;
1672         processedArgs.context = args[2];
1673       } else {
1674         processedArgs.fromCurrentState = value;
1675         value = args[2];
1676         if (SC.typeOf(value) === SC.T_BOOL) {
1677           processedArgs.useHistory = value;
1678         } else {
1679           processedArgs.context = value;
1680         }
1681       }
1682     }
1683     else {
1684       processedArgs.fromCurrentState = args[1];
1685       processedArgs.useHistory = args[2];
1686       processedArgs.context = args[3];
1687     }
1688 
1689     return processedArgs;
1690   },
1691 
1692   /** @private
1693 
1694     Will return a newly constructed root state class. The root state will have substates added to
1695     it based on properties found on this state that derive from a SC.State class. For the
1696     root state to be successfully built, the following much be met:
1697 
1698      - The rootStateExample property must be defined with a class that derives from SC.State
1699      - Either the initialState or statesAreConcurrent property must be set, but not both
1700      - There must be one or more states that can be added to the root state
1701 
1702   */
1703   _constructRootStateClass: function () {
1704     var rsExampleKey = 'rootStateExample',
1705         rsExample = this.get(rsExampleKey),
1706         initialState = this.get('initialState'),
1707         statesAreConcurrent = this.get('statesAreConcurrent'),
1708         stateCount = 0,
1709         key, value, valueIsFunc, attrs = {};
1710 
1711     if (SC.typeOf(rsExample) === SC.T_FUNCTION && rsExample.statePlugin) {
1712       rsExample = rsExample.apply(this);
1713     }
1714 
1715     if (!(SC.kindOf(rsExample, SC.State) && rsExample.isClass)) {
1716       this._logStatechartCreationError("Invalid root state example");
1717       return null;
1718     }
1719 
1720     if (statesAreConcurrent && !SC.empty(initialState)) {
1721       this._logStatechartCreationError("Can not assign an initial state when states are concurrent");
1722     } else if (statesAreConcurrent) {
1723       attrs.substatesAreConcurrent = YES;
1724     } else if (SC.typeOf(initialState) === SC.T_STRING) {
1725       attrs.initialSubstate = initialState;
1726     } else {
1727       this._logStatechartCreationError("Must either define initial state or assign states as concurrent");
1728       return null;
1729     }
1730 
1731     for (key in this) {
1732       if (key === rsExampleKey) continue;
1733 
1734       value = this[key];
1735       valueIsFunc = SC.typeOf(value) === SC.T_FUNCTION;
1736 
1737       if (valueIsFunc && value.statePlugin) {
1738         value = value.apply(this);
1739       }
1740 
1741       if (SC.kindOf(value, SC.State) && value.isClass && this[key] !== this.constructor) {
1742         attrs[key] = value;
1743         stateCount += 1;
1744       }
1745     }
1746 
1747     if (stateCount === 0) {
1748       this._logStatechartCreationError("Must define one or more states");
1749       return null;
1750     }
1751 
1752     return rsExample.extend(attrs);
1753   },
1754 
1755   /** @private */
1756   _logStatechartCreationError: function (msg) {
1757     SC.Logger.error("Unable to create statechart for %@: %@.".fmt(this, msg));
1758   },
1759 
1760   /**
1761     Used to log a statechart error message
1762   */
1763   statechartLogError: function (msg) {
1764     SC.Logger.error("ERROR %@: %@".fmt(this.get('statechartLogPrefix'), msg));
1765   },
1766 
1767   /**
1768     Used to log a statechart warning message
1769   */
1770   statechartLogWarning: function (msg) {
1771     if (this.get('suppressStatechartWarnings')) return;
1772     SC.Logger.warn("WARN %@: %@".fmt(this.get('statechartLogPrefix'), msg));
1773   },
1774 
1775   /** @property */
1776   statechartLogPrefix: function () {
1777     var className = SC._object_className(this.constructor),
1778         name = this.get('name'), prefix;
1779 
1780     if (SC.empty(name)) prefix = "%@<%@>".fmt(className, SC.guidFor(this));
1781     else prefix = "%@<%@, %@>".fmt(className, name, SC.guidFor(this));
1782 
1783     return prefix;
1784   }.property().cacheable()
1785 
1786 };
1787 
1788 SC.mixin(SC.StatechartManager, SC.StatechartDelegate, SC.DelegateSupport);
1789 
1790 /**
1791   The default name given to a statechart's root state
1792 */
1793 SC.ROOT_STATE_NAME = "__ROOT_STATE__";
1794 
1795 /**
1796   Constants used during the state transition process
1797 */
1798 SC.EXIT_STATE = 0;
1799 SC.ENTER_STATE = 1;
1800 
1801 /**
1802   A Statechart class.
1803 */
1804 SC.Statechart = SC.Object.extend(SC.StatechartManager, {
1805   autoInitStatechart: NO
1806 });
1807 
1808 SC.Statechart.design = SC.Statechart.extend;
1809