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 /*globals SC */
  9 
 10 //@if(debug)
 11 SC.TRACE_STATECHART_STYLE = {
 12   init: 'font-style: italic; font-weight: bold;', // Initialization
 13   action: 'color: #5922ab; font-style: italic; font-weight: bold;', // Actions and events
 14   actionInfo: 'color: #5922ab; font-style: italic;', // Actions and events
 15   route: 'color: #a67000; font-style: italic;', // Routing
 16   gotoState: 'color: #479a48; font-style: italic; font-weight: bold;', // Goto state
 17   gotoStateInfo: 'color: #479a48; font-style: italic;', // Goto state
 18   enter: 'color: #479a48; font-style: italic; font-weight: bold;', // Entering
 19   exit: 'color: #479a48; font-style: italic; font-weight: bold;' // Exiting
 20 };
 21 //@endif
 22 
 23 /**
 24   @class
 25 
 26   Represents a state within a statechart.
 27 
 28   The statechart actively manages all states belonging to it. When a state is created,
 29   it immediately registers itself with it parent states.
 30 
 31   You do not create an instance of a state itself. The statechart manager will go through its
 32   state heirarchy and create the states itself.
 33 
 34   For more information on using statecharts, see SC.StatechartManager.
 35 
 36   @author Michael Cohen
 37   @extends SC.Object
 38 */
 39 SC.State = SC.Object.extend(
 40   /** @lends SC.State.prototype */ {
 41 
 42   //@if(debug)
 43   /* BEGIN DEBUG ONLY PROPERTIES AND METHODS */
 44 
 45   /**
 46     Indicates if this state should trace actions. Useful for debugging
 47     purposes. Managed by the statechart.
 48 
 49     @see SC.StatechartManager#trace
 50 
 51     @type Boolean
 52   */
 53   trace: function() {
 54     var key = this.getPath('statechart.statechartTraceKey');
 55     return this.getPath('statechart.%@'.fmt(key));
 56   }.property().cacheable(),
 57 
 58   /** @private */
 59   _statechartTraceDidChange: function() {
 60     this.notifyPropertyChange('trace');
 61   },
 62 
 63   /**
 64     Used to log a state trace message
 65   */
 66   stateLogTrace: function(msg, style) {
 67     var sc = this.get('statechart');
 68     sc.statechartLogTrace("  %@: %@".fmt(this, msg), style);
 69   },
 70 
 71   /* END DEBUG ONLY PROPERTIES AND METHODS */
 72   //@endif
 73 
 74   /**
 75     The name of the state
 76 
 77     @type String
 78   */
 79   name: null,
 80 
 81   /**
 82     This state's parent state. Managed by the statechart
 83 
 84     @type State
 85   */
 86   parentState: null,
 87 
 88   /**
 89     This state's history state. Can be null. Managed by the statechart.
 90 
 91     @type State
 92   */
 93   historyState: null,
 94 
 95   /**
 96     Used to indicate the initial substate of this state to enter into.
 97 
 98     You assign the value with the name of the state. Upon creation of
 99     the state, the statechart will automatically change the property
100     to be a corresponding state object
101 
102     The substate is only to be this state's immediate substates. If
103     no initial substate is assigned then this states initial substate
104     will be an instance of an empty state (SC.EmptyState).
105 
106     Note that a statechart's root state must always have an explicity
107     initial substate value assigned else an error will be thrown.
108 
109     @property {String|State}
110   */
111   initialSubstate: null,
112 
113   /**
114     Used to indicates if this state's immediate substates are to be
115     concurrent (orthogonal) to each other.
116 
117     @type Boolean
118   */
119   substatesAreConcurrent: NO,
120 
121   /**
122     The immediate substates of this state. Managed by the statechart.
123 
124     @type Array
125   */
126   substates: null,
127 
128   /**
129     The statechart that this state belongs to. Assigned by the owning
130     statechart.
131 
132     @type Statechart
133   */
134   statechart: null,
135 
136   /**
137     Indicates if this state has been initialized by the statechart
138 
139     @propety {Boolean}
140   */
141   stateIsInitialized: NO,
142 
143   /**
144     An array of this state's current substates. Managed by the statechart
145 
146     @propety {Array}
147   */
148   currentSubstates: null,
149 
150   /**
151     An array of this state's substates that are currently entered. Managed by
152     the statechart.
153 
154     @type Array
155   */
156   enteredSubstates: null,
157 
158   /**
159     Can optionally assign what route this state is to represent.
160 
161     If assigned then this state will be notified to handle the route when triggered
162     any time the app's location changes and matches this state's assigned route.
163     The handler invoked is this state's {@link #routeTriggered} method.
164 
165     The value assigned to this property is dependent on the underlying routing
166     mechanism used by the application. The default routing mechanism is to use
167     SC.routes.
168 
169     @property {String|Hash}
170 
171     @see #routeTriggered
172     @see #location
173     @see SC.StatechartDelegate
174   */
175   representRoute: null,
176 
177   /**
178     Indicates who the owner is of this state. If not set on the statechart
179     then the owner is the statechart, otherwise it is the assigned
180     object. Managed by the statechart.
181 
182     @see SC.StatechartManager#owner
183 
184     @type SC.Object
185   */
186   owner: function() {
187     var sc = this.get('statechart'),
188         key = sc ? sc.get('statechartOwnerKey') : null,
189         owner = sc ? sc.get(key) : null;
190     return owner ? owner : sc;
191   }.property().cacheable(),
192 
193   /**
194     Returns the statechart's assigned delegate. A statechart delegate is one
195     that adheres to the {@link SC.StatechartDelegate} mixin.
196 
197     @type SC.Object
198 
199     @see SC.StatechartDelegate
200   */
201   statechartDelegate: function() {
202     return this.getPath('statechart.statechartDelegate');
203   }.property().cacheable(),
204 
205   /**
206     A volatile property used to get and set the app's current location.
207 
208     This computed property defers to the the statechart's delegate to
209     actually update and acquire the app's location.
210 
211     Note: Binding for this particular case is discouraged since in most
212     cases we need the location value immediately. If we were to use
213     bindings then the location value wouldn't be updated until at least
214     the end of one run loop. It is also advised that the delegate not
215     have its `statechartUpdateLocationForState` and
216     `statechartAcquireLocationForState` methods implemented where bindings
217     are used since they will inadvertenly stall the location value from
218     propogating immediately.
219 
220     @type String
221 
222     @see SC.StatechartDelegate#statechartUpdateLocationForState
223     @see SC.StatechartDelegate#statechartAcquireLocationForState
224   */
225   location: function(key, value) {
226     var sc = this.get('statechart'),
227         del = this.get('statechartDelegate');
228 
229     if (value !== undefined) {
230       del.statechartUpdateLocationForState(sc, value, this);
231     }
232 
233     return del.statechartAcquireLocationForState(sc, this);
234   }.property().idempotent(),
235 
236   init: function() {
237     sc_super();
238 
239     this._registeredEventHandlers = {};
240     this._registeredStringEventHandlers = {};
241     this._registeredRegExpEventHandlers = [];
242     this._registeredStateObserveHandlers = {};
243     this._registeredSubstatePaths = {};
244     this._registeredSubstates = [];
245     this._isEnteringState = NO;
246     this._isExitingState = NO;
247 
248     // Setting up observes this way is faster then using .observes,
249     // which adds a noticable increase in initialization time.
250 
251     var sc = this.get('statechart'),
252         ownerKey = sc ? sc.get('statechartOwnerKey') : null;
253 
254     if (sc) {
255       sc.addObserver(ownerKey, this, '_statechartOwnerDidChange');
256 
257       //@if(debug)
258       var traceKey = sc ? sc.get('statechartTraceKey') : null;
259       sc.addObserver(traceKey, this, '_statechartTraceDidChange');
260       //@endif
261     }
262   },
263 
264   destroy: function() {
265     var sc = this.get('statechart'),
266         ownerKey = sc ? sc.get('statechartOwnerKey') : null;
267 
268     if (sc) {
269       sc.removeObserver(ownerKey, this, '_statechartOwnerDidChange');
270 
271       //@if(debug)
272       var traceKey = sc ? sc.get('statechartTraceKey') : null;
273       sc.removeObserver(traceKey, this, '_statechartTraceDidChange');
274       //@endif
275     }
276 
277     var substates = this.get('substates');
278     if (substates) {
279       substates.forEach(function(state) {
280         state.destroy();
281       });
282     }
283 
284     this._teardownAllStateObserveHandlers();
285 
286     this.set('substates', null);
287     this.set('currentSubstates', null);
288     this.set('enteredSubstates', null);
289     this.set('parentState', null);
290     this.set('historyState', null);
291     this.set('initialSubstate', null);
292     this.set('statechart', null);
293 
294     //@if(debug)
295     this.notifyPropertyChange('trace');
296     //@endif
297 
298     this.notifyPropertyChange('owner');
299 
300     this._registeredEventHandlers = null;
301     this._registeredStringEventHandlers = null;
302     this._registeredRegExpEventHandlers = null;
303     this._registeredStateObserveHandlers = null;
304     this._registeredSubstatePaths = null;
305     this._registeredSubstates = null;
306 
307     sc_super();
308   },
309 
310   /**
311     Used to initialize this state. To only be called by the owning statechart.
312   */
313   initState: function() {
314     if (this.get('stateIsInitialized')) return;
315 
316     this._registerWithParentStates();
317     this._setupRouteHandling();
318 
319     var key = null,
320         value = null,
321         state = null,
322         substates = [],
323         matchedInitialSubstate = NO,
324         initialSubstate = this.get('initialSubstate'),
325         substatesAreConcurrent = this.get('substatesAreConcurrent'),
326         valueIsFunc = NO,
327         historyState = null;
328 
329     this.set('substates', substates);
330 
331     if (SC.kindOf(initialSubstate, SC.HistoryState) && initialSubstate.isClass) {
332       historyState = this.createSubstate(initialSubstate);
333       this.set('initialSubstate', historyState);
334 
335       if (SC.none(historyState.get('defaultState'))) {
336         this.stateLogError("Initial substate is invalid. History state requires the name of a default state to be set");
337         this.set('initialSubstate', null);
338         historyState = null;
339       }
340     }
341 
342     // Iterate through all this state's substates, if any, create them, and then initialize
343     // them. This causes a recursive process.
344     for (key in this) {
345       value = this[key];
346       valueIsFunc = SC.typeOf(value) === SC.T_FUNCTION;
347 
348       if (valueIsFunc && value.isEventHandler) {
349         this._registerEventHandler(key, value);
350         continue;
351       }
352 
353       if (valueIsFunc && value.isStateObserveHandler) {
354         this._registerStateObserveHandler(key, value);
355         continue;
356       }
357 
358       if (valueIsFunc && value.statePlugin) {
359         value = value.apply(this);
360       }
361 
362       if (SC.kindOf(value, SC.State) && value.isClass && this[key] !== this.constructor) {
363         state = this._addSubstate(key, value);
364         if (key === initialSubstate) {
365           this.set('initialSubstate', state);
366           matchedInitialSubstate = YES;
367         } else if (historyState && historyState.get('defaultState') === key) {
368           historyState.set('defaultState', state);
369           matchedInitialSubstate = YES;
370         }
371       }
372     }
373 
374     if (!SC.none(initialSubstate) && !matchedInitialSubstate) {
375       this.stateLogError("Unable to set initial substate %@ since it did not match any of state's %@ substates".fmt(initialSubstate, this));
376     }
377 
378     if (substates.length === 0) {
379       if (!SC.none(initialSubstate)) {
380         this.stateLogWarning("Unable to make %@ an initial substate since state %@ has no substates".fmt(initialSubstate, this));
381       }
382     }
383     else if (substates.length > 0) {
384       state = this._addEmptyInitialSubstateIfNeeded();
385       if (!state && initialSubstate && substatesAreConcurrent) {
386         this.set('initialSubstate', null);
387         this.stateLogWarning("Can not use %@ as initial substate since substates are all concurrent for state %@".fmt(initialSubstate, this));
388       }
389     }
390 
391     this.notifyPropertyChange('substates');
392     this.set('currentSubstates', []);
393     this.set('enteredSubstates', []);
394     this.set('stateIsInitialized', YES);
395   },
396 
397   /** @private
398 
399     Used to bind this state with a route this state is to represent if a route has been assigned.
400 
401     When invoked, the method will delegate the actual binding strategy to the statechart delegate
402     via the delegate's {@link SC.StatechartDelegate#statechartBindStateToRoute} method.
403 
404     Note that a state cannot be bound to a route if this state is a concurrent state.
405 
406     @see #representRoute
407     @see SC.StatechartDelegate#statechartBindStateToRoute
408   */
409   _setupRouteHandling: function() {
410     var route = this.get('representRoute'),
411         sc = this.get('statechart'),
412         del = this.get('statechartDelegate');
413 
414     if (SC.none(route)) return;
415 
416     if (this.get('isConcurrentState')) {
417       this.stateLogError("State %@ cannot handle route '%@' since state is concurrent".fmt(this, route));
418       return;
419     }
420 
421     del.statechartBindStateToRoute(sc, this, route, this.routeTriggered);
422   },
423 
424   /**
425     Main handler that gets triggered whenever the app's location matches this state's assigned
426     route.
427 
428     When invoked the handler will first refer to the statechart delegate to determine if it
429     should actually handle the route via the delegate's
430     {@see SC.StatechartDelegate#statechartShouldStateHandleTriggeredRoute} method. If the
431     delegate allows the handling of the route then the state will continue on with handling
432     the triggered route by calling the state's {@link #handleTriggeredRoute} method, otherwise
433     the state will cancel the handling and inform the delegate through the delegate's
434     {@see SC.StatechartDelegate#statechartStateCancelledHandlingRoute} method.
435 
436     The handler will create a state route context ({@link SC.StateRouteContext}) object
437     that packages information about what is being currently handled. This context object gets
438     passed along to the delegate's invoked methods as well as the state transition process.
439 
440     Note that this method is not intended to be directly called or overridden.
441 
442     @see #representRoute
443     @see SC.StatechartDelegate#statechartShouldStateHandleRoute
444     @see SC.StatechartDelegate#statechartStateCancelledHandlingRoute
445     @see #createStateRouteHandlerContext
446     @see #handleTriggeredRoute
447   */
448   routeTriggered: function(params) {
449     if (this._isEnteringState) return;
450 
451     var sc = this.get('statechart'),
452         del = this.get('statechartDelegate'),
453         loc = this.get('location');
454 
455     var attr = {
456       state: this,
457       location: loc,
458       params: params,
459       handler: this.routeTriggered
460     };
461 
462     var context = this.createStateRouteHandlerContext(attr);
463 
464     if (del.statechartShouldStateHandleTriggeredRoute(sc, this, context)) {
465       //@if(debug)
466       if (this.get('trace') && loc) {
467         this.stateLogTrace("will handle route '%@'".fmt(loc), SC.TRACE_STATECHART_STYLE.route);
468       }
469       //@endif
470       this.handleTriggeredRoute(context);
471     } else {
472       del.statechartStateCancelledHandlingTriggeredRoute(sc, this, context);
473     }
474   },
475 
476   /**
477     Constructs a new instance of a state routing context object.
478 
479     @param {Hash} attr attributes to apply to the constructed object
480     @return {SC.StateRouteContext}
481 
482     @see #handleRoute
483   */
484   createStateRouteHandlerContext: function(attr) {
485     return SC.StateRouteHandlerContext.create(attr);
486   },
487 
488   /**
489     Invoked by this state's {@link #routeTriggered} method if the state is
490     actually allowed to handle the triggered route.
491 
492     By default the method invokes a state transition to this state.
493   */
494   handleTriggeredRoute: function(context) {
495     this.gotoState(this, context);
496   },
497 
498   /** @private */
499   _addEmptyInitialSubstateIfNeeded: function() {
500     var initialSubstate = this.get('initialSubstate'),
501         substatesAreConcurrent = this.get('substatesAreConcurrent');
502 
503     if (initialSubstate || substatesAreConcurrent) return null;
504 
505     var state = this.createSubstate(SC.EmptyState);
506     this.set('initialSubstate', state);
507     this.get('substates').push(state);
508     this[state.get('name')] = state;
509     state.initState();
510     this.stateLogWarning("state %@ has no initial substate defined. Will default to using an empty state as initial substate".fmt(this));
511     return state;
512   },
513 
514   /** @private */
515   _addSubstate: function(name, state, attr) {
516     var substates = this.get('substates');
517 
518     attr = SC.clone(attr) || {};
519     attr.name = name;
520     state = this.createSubstate(state, attr);
521     substates.push(state);
522     this[name] = state;
523     state.initState();
524     return state;
525   },
526 
527   /**
528     Used to dynamically add a substate to this state. Once added successfully you
529     are then able to go to it from any other state within the owning statechart.
530 
531     A couple of notes when adding a substate:
532 
533     - If this state does not have any substates, then in addition to the
534       substate being added, an empty state will also be added and set as the
535       initial substate. To make the added substate the initial substate, set
536       this object's initialSubstate property.
537 
538     - If this state is a current state, the added substate will not be entered.
539 
540     - If this state is entered and its substates are concurrent, the added
541       substate will not be entered.
542 
543     If this state is either entered or current and you'd like the added substate
544     to take affect, you will need to explicitly reenter this state by calling
545     its `reenter` method.
546 
547     Be aware that the name of the state you are adding must not conflict with
548     the name of a property on this state or else you will get an error.
549     In addition, this state must be initialized to add substates.
550 
551     @param {String} name a unique name for the given substate.
552     @param {SC.State} state a class that derives from `SC.State`
553     @param {Object} [attr] literal to be applied to the substate
554     @returns {SC.State} an instance of the given state class
555   */
556   addSubstate: function(name, state, attr) {
557     if (SC.empty(name)) {
558       this.stateLogError("Can not add substate. name required");
559       return null;
560     }
561 
562     if (this[name] !== undefined) {
563       this.stateLogError("Can not add substate '%@'. Already a defined property".fmt(name));
564       return null;
565     }
566 
567     if (!this.get('stateIsInitialized')) {
568       this.stateLogError("Can not add substate '%@'. this state is not yet initialized".fmt(name));
569       return null;
570     }
571 
572     var len = arguments.length;
573 
574     if (len === 1) {
575       state = SC.State;
576     } else if (len === 2 && SC.typeOf(state) === SC.T_HASH) {
577       attr = state;
578       state = SC.State;
579     }
580 
581     var stateIsValid = SC.kindOf(state, SC.State) && state.isClass;
582 
583     if (!stateIsValid) {
584       this.stateLogError("Can not add substate '%@'. must provide a state class".fmt(name));
585       return null;
586     }
587 
588     state = this._addSubstate(name, state, attr);
589     this._addEmptyInitialSubstateIfNeeded();
590     this.notifyPropertyChange('substates');
591 
592     return state;
593   },
594 
595   /**
596     creates a substate for this state
597   */
598   createSubstate: function(state, attr) {
599     attr = attr || {};
600     return state.create({
601       parentState: this,
602       statechart: this.get('statechart')
603     }, attr);
604   },
605 
606   /** @private
607 
608     Registers event handlers with this state. Event handlers are special
609     functions on the state that are intended to handle more than one event. This
610     compared to basic functions that only respond to a single event that reflects
611     the name of the method.
612   */
613   _registerEventHandler: function(name, handler) {
614     var events = handler.events,
615         event = null,
616         len = events.length,
617         i = 0;
618 
619     this._registeredEventHandlers[name] = handler;
620 
621     for (; i < len; i += 1) {
622       event = events[i];
623 
624       if (SC.typeOf(event) === SC.T_STRING) {
625         this._registeredStringEventHandlers[event] = {
626           name: name,
627           handler: handler
628         };
629         continue;
630       }
631 
632       if (event instanceof RegExp) {
633         this._registeredRegExpEventHandlers.push({
634           name: name,
635           handler: handler,
636           regexp: event
637         });
638         continue;
639       }
640 
641       this.stateLogError("Invalid event %@ for event handler %@ in state %@".fmt(event, name, this));
642     }
643   },
644 
645   /** @private
646 
647     Registers state observe handlers with this state. State observe handlers behave just like
648     when you apply observes() on a method but will only be active when the state is currently
649     entered, otherwise the handlers are inactive until the next time the state is entered
650   */
651   _registerStateObserveHandler: function(name, handler) {
652     var i = 0,
653         args = handler.args,
654         len = args.length,
655         arg, validHandlers = YES;
656 
657     for (; i < len; i += 1) {
658       arg = args[i];
659       if (SC.typeOf(arg) !== SC.T_STRING || SC.empty(arg)) {
660         this.stateLogError("Invalid argument %@ for state observe handler %@ in state %@".fmt(arg, name, this));
661         validHandlers = NO;
662       }
663     }
664 
665     if (!validHandlers) return;
666 
667     this._registeredStateObserveHandlers[name] = handler.args;
668   },
669 
670   /** @private
671     Will traverse up through this state's parent states to register
672     this state with them.
673   */
674   _registerWithParentStates: function() {
675     var parent = this.get('parentState');
676     while (!SC.none(parent)) {
677       parent._registerSubstate(this);
678       parent = parent.get('parentState');
679     }
680   },
681 
682   /** @private
683     Will register a given state as a substate of this state
684   */
685   _registerSubstate: function(state) {
686     var path = state.pathRelativeTo(this);
687     if (SC.none(path)) return;
688 
689     this._registeredSubstates.push(state);
690 
691     // Keep track of states based on their relative path
692     // to this state.
693     var regPaths = this._registeredSubstatePaths;
694     if (regPaths[state.get('name')] === undefined) {
695       regPaths[state.get('name')] = { };
696     }
697 
698     var paths = regPaths[state.get('name')];
699     paths[path] = state;
700   },
701 
702   /**
703     Will generate path for a given state that is relative to this state. It is
704     required that the given state is a substate of this state.
705 
706     If the heirarchy of the given state to this state is the following:
707     A > B > C, where A is this state and C is the given state, then the
708     relative path generated will be "B.C"
709   */
710   pathRelativeTo: function(state) {
711     var path = this.get('name'),
712         parent = this.get('parentState');
713 
714     while (!SC.none(parent) && parent !== state) {
715       path = "%@.%@".fmt(parent.get('name'), path);
716       parent = parent.get('parentState');
717     }
718 
719     if (parent !== state && state !== this) {
720       this.stateLogError('Can not generate relative path from %@ since it not a parent state of %@'.fmt(state, this));
721       return null;
722     }
723 
724     return path;
725   },
726 
727   /**
728     Used to get a substate of this state that matches a given value.
729 
730     If the value is a state object, then the value will be returned if it is indeed
731     a substate of this state, otherwise null is returned.
732 
733     If the given value is a string, then the string is assumed to be a path expression
734     to a substate. The value is then parsed to find the closest match. For path expression
735     syntax, refer to the {@link SC.StatePathMatcher} class.
736 
737     If there is no match then null is returned. If there is more than one match then null
738     is return and an error is generated indicating ambiguity of the given value.
739 
740     An optional callback can be provided to handle the scenario when either no
741     substate is found or there is more than one match. The callback is then given
742     the opportunity to further handle the outcome and return a result which the
743     getSubstate method will then return. The callback should have the following
744     signature:
745 
746       function(state, value, paths)
747 
748     - state: The state getState was invoked on
749     - value: The value supplied to getState
750     - paths: An array of substate paths that matched the given value
751 
752     If there were no matches then `paths` is not provided to the callback.
753 
754     You can also optionally provide a target that the callback is invoked on. If no
755     target is provided then this state is used as the target.
756 
757     @param value {State|String} used to identify a substate of this state
758     @param [callback] {Function} the callback
759     @param [target] {Object} the target
760   */
761   getSubstate: function(value, callback, target) {
762     if (!value) return null;
763 
764     var valueType = SC.typeOf(value);
765 
766     // If the value is an object then just check if the value is
767     // a registered substate of this state, and if so return it.
768     if (valueType === SC.T_OBJECT) {
769       return this._registeredSubstates.indexOf(value) > -1 ? value : null;
770     }
771 
772     if (valueType !== SC.T_STRING) {
773       this.stateLogError("Can not find matching subtype. value must be an object or string: %@".fmt(value));
774       return null;
775     }
776 
777     var matcher = SC.StatePathMatcher.create({ state: this, expression: value }),
778         matches = [], key;
779 
780     if (matcher.get('tokens').length === 0) return null;
781 
782     var paths = this._registeredSubstatePaths[matcher.get('lastPart')];
783     if (!paths) return this._notifySubstateNotFound(callback, target, value);
784 
785     for (key in paths) {
786       if (matcher.match(key)) {
787         matches.push(paths[key]);
788       }
789     }
790 
791     if (matches.length === 1) return matches[0];
792 
793     if (matches.length > 1) {
794       var keys = [];
795       for (key in paths) { keys.push(key); }
796 
797       if (callback) return this._notifySubstateNotFound(callback, target, value, keys);
798 
799       var msg = "Can not find substate matching '%@' in state %@. Ambiguous with the following: %@";
800       this.stateLogError(msg.fmt(value, this.get('fullPath'), keys.join(', ')));
801     }
802 
803     return this._notifySubstateNotFound(callback, target, value);
804   },
805 
806   /** @private */
807   _notifySubstateNotFound: function(callback, target, value, keys) {
808     return callback ? callback.call(target || this, this, value, keys) : null;
809   },
810 
811   /**
812     Will attempt to get a state relative to this state.
813 
814     A state is returned based on the following:
815 
816     1. First check this state's substates for a match; and
817     2. If no matching substate then attempt to get the state from
818        this state's parent state.
819 
820     Therefore states are recursively traversed up to the root state
821     to identify a match, and if found is ultimately returned, otherwise
822     null is returned. In the case that the value supplied is ambiguous
823     an error message is returned.
824 
825     The value provided can either be a state object or a state path expression.
826     For path expression syntax, refer to the {@link SC.StatePathMatcher} class.
827   */
828   getState: function(value) {
829     if (value === this.get('name')) return this;
830     if (SC.kindOf(value, SC.State)) return value;
831     return this.getSubstate(value, this._handleSubstateNotFound);
832   },
833 
834   /** @private */
835   _handleSubstateNotFound: function(state, value, keys) {
836     var parentState = this.get('parentState');
837 
838     if (parentState) return parentState.getState(value);
839 
840     if (keys) {
841       var msg = "Can not find state matching '%@'. Ambiguous with the following: %@";
842       this.stateLogError(msg.fmt(value, keys.join(', ')));
843     }
844 
845     return null;
846   },
847 
848   /**
849     Used to go to a state in the statechart either directly from this state if it is a current state,
850     or from the first relative current state from this state.
851 
852     If the value given is a string then it is considered a state path expression. The path is then
853     used to find a state relative to this state based on rules of the {@link #getState} method.
854 
855     @param value {SC.State|String} the state to go to
856     @param [context] {Hash|Object} context object that will be supplied to all states that are
857            exited and entered during the state transition process. Context can not be an instance of
858            SC.State.
859   */
860   gotoState: function(value, context) {
861     var state = this.getState(value);
862 
863     if (!state) {
864       var msg = "can not go to state %@ from state %@. Invalid value.";
865       this.stateLogError(msg.fmt(value, this));
866       return;
867     }
868 
869     var from = this.findFirstRelativeCurrentState(state);
870     this.get('statechart').gotoState(state, from, false, context);
871   },
872 
873   /**
874     Used to go to a given state's history state in the statechart either directly from this state if it
875     is a current state or from one of this state's current substates.
876 
877     If the value given is a string then it is considered a state path expression. The path is then
878     used to find a state relative to this state based on rules of the {@link #getState} method.
879 
880     Method can be called in the following ways:
881 
882         // With one argument
883         gotoHistoryState(<value>)
884 
885         // With two arguments
886         gotoHistoryState(<value>, <boolean | hash>)
887 
888         // With three arguments
889         gotoHistoryState(<value>, <boolean>, <hash>)
890 
891     Where <value> is either a string or a SC.State object and <hash> is a regular JS hash object.
892 
893     @param value {SC.State|String} the state whose history state to go to
894     @param [recusive] {Boolean} indicates whether to follow history states recusively starting
895            from the given state
896     @param [context] {Hash|Object} context object that will be supplied to all states that are exited
897            entered during the state transition process. Context can not be an instance of SC.State.
898   */
899   gotoHistoryState: function(value, recursive, context) {
900     var state = this.getState(value);
901 
902     if (!state) {
903       var msg = "can not go to history state %@ from state %@. Invalid value.";
904       this.stateLogError(msg.fmt(value, this));
905       return;
906     }
907 
908     var from = this.findFirstRelativeCurrentState(state);
909     this.get('statechart').gotoHistoryState(state, from, recursive, context);
910   },
911 
912   /**
913     Resumes an active goto state transition process that has been suspended.
914   */
915   resumeGotoState: function() {
916     this.get('statechart').resumeGotoState();
917   },
918 
919   /**
920     Used to check if a given state is a current substate of this state. Mainly used in cases
921     when this state is a concurrent state.
922 
923     @param state {State|String} either a state object or the name of a state
924     @returns {Boolean} true is the given state is a current substate, otherwise false is returned
925   */
926   stateIsCurrentSubstate: function(state) {
927     if (SC.typeOf(state) === SC.T_STRING) state = this.get('statechart').getState(state);
928     var current = this.get('currentSubstates');
929     return !!current && current.indexOf(state) >= 0;
930   },
931 
932   /**
933     Used to check if a given state is a substate of this state that is currently entered.
934 
935     @param state {State|String} either a state object of the name of a state
936     @returns {Boolean} true if the given state is a entered substate, otherwise false is returned
937   */
938   stateIsEnteredSubstate: function(state) {
939     if (SC.typeOf(state) === SC.T_STRING) state = this.get('statechart').getState(state);
940     var entered = this.get('enteredSubstates');
941     return !!entered && entered.indexOf(state) >= 0;
942   },
943 
944   /**
945     Indicates if this state is the root state of the statechart.
946 
947     @type Boolean
948   */
949   isRootState: function() {
950     return this.getPath('statechart.rootState') === this;
951   }.property(),
952 
953   /**
954     Indicates if this state is a current state of the statechart.
955 
956     @type Boolean
957   */
958   isCurrentState: function() {
959     return this.stateIsCurrentSubstate(this);
960   }.property('currentSubstates').cacheable(),
961 
962   /**
963     Indicates if this state is a concurrent state
964 
965     @type Boolean
966   */
967   isConcurrentState: function() {
968     return this.getPath('parentState.substatesAreConcurrent');
969   }.property(),
970 
971   /**
972     Indicates if this state is a currently entered state.
973 
974     A state is currently entered if during a state transition process the
975     state's enterState method was invoked, but only after its exitState method
976     was called, if at all.
977   */
978   isEnteredState: function() {
979     return this.stateIsEnteredSubstate(this);
980   }.property('enteredSubstates').cacheable(),
981 
982   /**
983     Indicate if this state has any substates
984 
985     @propety {Boolean}
986   */
987   hasSubstates: function() {
988     return this.getPath('substates.length') > 0;
989   }.property('substates'),
990 
991   /**
992     Indicates if this state has any current substates
993   */
994   hasCurrentSubstates: function() {
995     var current = this.get('currentSubstates');
996     return !!current && current.get('length') > 0;
997   }.property('currentSubstates').cacheable(),
998 
999   /**
1000     Indicates if this state has any currently entered substates
1001   */
1002   hasEnteredSubstates: function() {
1003     var entered = this.get('enteredSubstates');
1004     return !!entered  && entered.get('length') > 0;
1005   }.property('enteredSubstates').cacheable(),
1006 
1007   /**
1008     Will attempt to find a current state in the statechart that is relative to
1009     this state.
1010 
1011     Ordered set of rules to find a relative current state:
1012 
1013       1. If this state is a current state then it will be returned
1014 
1015       2. If this state has no current states and this state has a parent state then
1016         return parent state's first relative current state, otherwise return null
1017 
1018       3. If this state has more than one current state then use the given anchor state
1019          to get a corresponding substate that can be used to find a current state relative
1020          to the substate, if a substate was found.
1021 
1022       4. If (3) did not find a relative current state then default to returning
1023          this state's first current substate.
1024 
1025     @param anchor {State|String} Optional. a substate of this state used to help direct
1026       finding a current state
1027     @return {SC.State} a current state
1028   */
1029   findFirstRelativeCurrentState: function(anchor) {
1030     if (this.get('isCurrentState')) return this;
1031 
1032     var currentSubstates = this.get('currentSubstates') || [],
1033         numCurrent = currentSubstates.get('length'),
1034         parent = this.get('parentState');
1035 
1036     if (numCurrent === 0) {
1037       return parent ? parent.findFirstRelativeCurrentState() : null;
1038     }
1039 
1040     if (numCurrent > 1) {
1041       anchor = this.getSubstate(anchor);
1042       if (anchor) return anchor.findFirstRelativeCurrentState();
1043     }
1044 
1045     return currentSubstates[0];
1046   },
1047 
1048   /**
1049     Used to re-enter this state. Call this only when the state a current state of
1050     the statechart.
1051   */
1052   reenter: function() {
1053     if (this.get('isEnteredState')) {
1054       this.gotoState(this);
1055     } else {
1056        SC.Logger.error('Can not re-enter state %@ since it is not an entered state in the statechart'.fmt(this));
1057     }
1058   },
1059 
1060   /**
1061     Called by the statechart to allow a state to try and handle the given event. If the
1062     event is handled by the state then YES is returned, otherwise NO.
1063 
1064     There is a particular order in how an event is handled by a state:
1065 
1066      1. Basic function whose name matches the event
1067      2. Registered event handler that is associated with an event represented as a string
1068      3. Registered event handler that is associated with events matching a regular expression
1069      4. The unknownEvent function
1070 
1071     Use of event handlers that are associated with events matching a regular expression may
1072     incur a performance hit, so they should be used sparingly.
1073 
1074     The unknownEvent function is only invoked if the state has it, otherwise it is skipped. Note that
1075     you should be careful when using unknownEvent since it can be either abused or cause unexpected
1076     behavior.
1077 
1078     Example of a state using all four event handling techniques:
1079 
1080         SC.State.extend({
1081 
1082           // Basic function handling event 'foo'
1083           foo: function(arg1, arg2) { ... },
1084 
1085           // event handler that handles 'frozen' and 'canuck'
1086           eventHandlerA: function(event, arg1, arg2) {
1087             ...
1088           }.handleEvent('frozen', 'canuck'),
1089 
1090           // event handler that handles events matching the regular expression /num\d/
1091           //   ex. num1, num2
1092           eventHandlerB: function(event, arg1, arg2) {
1093             ...
1094           }.handleEvent(/num\d/),
1095 
1096           // Handle any event that was not handled by some other
1097           // method on the state
1098           unknownEvent: function(event, arg1, arg2) {
1099 
1100           }
1101 
1102         });
1103   */
1104   tryToHandleEvent: function(event, arg1, arg2) {
1105     //@if(debug)
1106     var trace = this.get('trace');
1107     //@endif
1108 
1109     var sc = this.get('statechart'),
1110         ret;
1111 
1112     // First check if the name of the event is the same as a registered event handler. If so,
1113     // then do not handle the event.
1114     if (this._registeredEventHandlers[event]) {
1115       this.stateLogWarning("state %@ can not handle event '%@' since it is a registered event handler".fmt(this, event));
1116       return NO;
1117     }
1118 
1119     if (this._registeredStateObserveHandlers[event]) {
1120       this.stateLogWarning("state %@ can not handle event '%@' since it is a registered state observe handler".fmt(this, event));
1121       return NO;
1122     }
1123 
1124     // Now begin by trying a basic method on the state to respond to the event
1125     if (SC.typeOf(this[event]) === SC.T_FUNCTION) {
1126       //@if(debug)
1127       if (trace) this.stateLogTrace("will handle event '%@'".fmt(event), SC.TRACE_STATECHART_STYLE.actionInfo);
1128       //@endif
1129       sc.stateWillTryToHandleEvent(this, event, event);
1130       ret = (this[event](arg1, arg2) !== NO);
1131       sc.stateDidTryToHandleEvent(this, event, event, ret);
1132       return ret;
1133     }
1134 
1135     // Try an event handler that is associated with an event represented as a string
1136     var handler = this._registeredStringEventHandlers[event];
1137     if (handler) {
1138       //@if(debug)
1139       if (trace) this.stateLogTrace("%@ will handle event '%@'".fmt(handler.name, event), SC.TRACE_STATECHART_STYLE.actionInfo);
1140       //@endif
1141       sc.stateWillTryToHandleEvent(this, event, handler.name);
1142       ret = (handler.handler.call(this, event, arg1, arg2) !== NO);
1143       sc.stateDidTryToHandleEvent(this, event, handler.name, ret);
1144       return ret;
1145     }
1146 
1147     // Try an event handler that is associated with events matching a regular expression
1148 
1149     var len = this._registeredRegExpEventHandlers.length,
1150         i = 0;
1151 
1152     for (; i < len; i += 1) {
1153       handler = this._registeredRegExpEventHandlers[i];
1154       if (event.match(handler.regexp)) {
1155         //@if(debug)
1156         if (trace) this.stateLogTrace("%@ will handle event '%@'".fmt(handler.name, event), SC.TRACE_STATECHART_STYLE.actionInfo);
1157         //@endif
1158         sc.stateWillTryToHandleEvent(this, event, handler.name);
1159         ret = (handler.handler.call(this, event, arg1, arg2) !== NO);
1160         sc.stateDidTryToHandleEvent(this, event, handler.name, ret);
1161         return ret;
1162       }
1163     }
1164 
1165     // Final attempt. If the state has an unknownEvent function then invoke it to
1166     // handle the event
1167     if (SC.typeOf(this['unknownEvent']) === SC.T_FUNCTION) {
1168       //@if(debug)
1169       if (trace) this.stateLogTrace("unknownEvent will handle event '%@'".fmt(event), SC.TRACE_STATECHART_STYLE.actionInfo);
1170       //@endif
1171       sc.stateWillTryToHandleEvent(this, event, 'unknownEvent');
1172       ret = (this.unknownEvent(event, arg1, arg2) !== NO);
1173       sc.stateDidTryToHandleEvent(this, event, 'unknownEvent', ret);
1174       return ret;
1175     }
1176 
1177     // Nothing was able to handle the given event for this state
1178     return NO;
1179   },
1180 
1181   /**
1182     Called whenever this state is to be entered during a state transition process. This
1183     is useful when you want the state to perform some initial set up procedures.
1184 
1185     If when entering the state you want to perform some kind of asynchronous action, such
1186     as an animation or fetching remote data, then you need to return an asynchronous
1187     action, which is done like so:
1188 
1189         enterState: function() {
1190           return this.performAsync('foo');
1191         }
1192 
1193     After returning an action to be performed asynchronously, the statechart will suspend
1194     the active state transition process. In order to resume the process, you must call
1195     this state's resumeGotoState method or the statechart's resumeGotoState. If no asynchronous
1196     action is to be perform, then nothing needs to be returned.
1197 
1198     When the enterState method is called, an optional context value may be supplied if
1199     one was provided to the gotoState method.
1200 
1201     In the case that the context being supplied is a state context object
1202     ({@link SC.StateRouteHandlerContext}), an optional `enterStateByRoute` method can be invoked
1203     on this state if the state has implemented the method. If `enterStateByRoute` is
1204     not part of this state then the `enterState` method will be invoked by default. The
1205     `enterStateByRoute` is simply a convenience method that helps removes checks to
1206     determine if the context provide is a state route context object.
1207 
1208     @param {Hash} [context] value if one was supplied to gotoState when invoked
1209 
1210     @see #representRoute
1211   */
1212   enterState: function(context) { },
1213 
1214   /**
1215     Notification called just before enterState is invoked.
1216 
1217     Note: This is intended to be used by the owning statechart but it can be overridden if
1218     you need to do something special.
1219 
1220     @param {Hash} [context] value if one was supplied to gotoState when invoked
1221     @see #enterState
1222   */
1223   stateWillBecomeEntered: function(context) {
1224     this._isEnteringState = YES;
1225   },
1226 
1227   /**
1228     Notification called just after enterState is invoked.
1229 
1230     Note: This is intended to be used by the owning statechart but it can be overridden if
1231     you need to do something special.
1232 
1233     @param context {Hash} Optional value if one was supplied to gotoState when invoked
1234     @see #enterState
1235   */
1236   stateDidBecomeEntered: function(context) {
1237     this._setupAllStateObserveHandlers();
1238     this._isEnteringState = NO;
1239   },
1240 
1241   /**
1242     Called whenever this state is to be exited during a state transition process. This is
1243     useful when you want the state to peform some clean up procedures.
1244 
1245     If when exiting the state you want to perform some kind of asynchronous action, such
1246     as an animation or fetching remote data, then you need to return an asynchronous
1247     action, which is done like so:
1248 
1249         exitState: function() {
1250           return this.performAsync('foo');
1251         }
1252 
1253     After returning an action to be performed asynchronously, the statechart will suspend
1254     the active state transition process. In order to resume the process, you must call
1255     this state's resumeGotoState method or the statechart's resumeGotoState. If no asynchronous
1256     action is to be perform, then nothing needs to be returned.
1257 
1258     When the exitState method is called, an optional context value may be supplied if
1259     one was provided to the gotoState method.
1260 
1261     @param context {Hash} Optional value if one was supplied to gotoState when invoked
1262   */
1263   exitState: function(context) { },
1264 
1265   /**
1266     Notification called just before exitState is invoked.
1267 
1268     Note: This is intended to be used by the owning statechart but it can be overridden
1269     if you need to do something special.
1270 
1271     @param context {Hash} Optional value if one was supplied to gotoState when invoked
1272     @see #exitState
1273   */
1274   stateWillBecomeExited: function(context) {
1275     this._isExitingState = YES;
1276     this._teardownAllStateObserveHandlers();
1277   },
1278 
1279   /**
1280     Notification called just after exitState is invoked.
1281 
1282     Note: This is intended to be used by the owning statechart but it can be overridden
1283     if you need to do something special.
1284 
1285     @param context {Hash} Optional value if one was supplied to gotoState when invoked
1286     @see #exitState
1287   */
1288   stateDidBecomeExited: function(context) {
1289     this._isExitingState = NO;
1290   },
1291 
1292   /** @private
1293 
1294     Used to setup all the state observer handlers. Should be done when
1295     the state has been entered.
1296   */
1297   _setupAllStateObserveHandlers: function() {
1298     this._configureAllStateObserveHandlers('addObserver');
1299   },
1300 
1301   /** @private
1302 
1303     Used to teardown all the state observer handlers. Should be done when
1304     the state is being exited.
1305   */
1306   _teardownAllStateObserveHandlers: function() {
1307     this._configureAllStateObserveHandlers('removeObserver');
1308   },
1309 
1310   /** @private
1311 
1312     Primary method used to either add or remove this state as an observer
1313     based on all the state observe handlers that have been registered with
1314     this state.
1315 
1316     Note: The code to add and remove the state as an observer has been
1317     taken from the observerable mixin and made slightly more generic. However,
1318     having this code in two different places is not ideal, but for now this
1319     will have to do. In the future the code should be refactored so that
1320     there is one common function that both the observerable mixin and the
1321     statechart framework use.
1322   */
1323   _configureAllStateObserveHandlers: function(action) {
1324     var key, values, dotIndex, path, observer, i, root;
1325 
1326     for (key in this._registeredStateObserveHandlers) {
1327       values = this._registeredStateObserveHandlers[key];
1328       for (i = 0; i < values.length; i += 1) {
1329         path = values[i]; observer = key;
1330 
1331         // Use the dot index in the path to determine how the state
1332         // should add itself as an observer.
1333 
1334         dotIndex = path.indexOf('.');
1335 
1336         if (dotIndex < 0) {
1337           this[action](path, this, observer);
1338         } else if (path.indexOf('*') === 0) {
1339           this[action](path.slice(1), this, observer);
1340         } else {
1341           root = null;
1342 
1343           if (dotIndex === 0) {
1344             root = this; path = path.slice(1);
1345           } else if (dotIndex === 4 && path.slice(0, 5) === 'this.') {
1346             root = this; path = path.slice(5);
1347           } else if (dotIndex < 0 && path.length === 4 && path === 'this') {
1348             root = this; path = '';
1349           }
1350 
1351           SC.Observers[action](path, this, observer, root);
1352         }
1353       }
1354     }
1355   },
1356 
1357   /**
1358     Call when an asynchronous action need to be performed when either entering or exiting
1359     a state.
1360 
1361     @see enterState
1362     @see exitState
1363   */
1364   performAsync: function(func, arg1, arg2) {
1365     return SC.Async.perform(func, arg1, arg2);
1366   },
1367 
1368   /** @override
1369 
1370     Returns YES if this state can respond to the given event, otherwise
1371     NO is returned
1372 
1373     @param event {String} the value to check
1374     @returns {Boolean}
1375   */
1376   respondsToEvent: function(event) {
1377     if (this._registeredEventHandlers[event]) return false;
1378     if (SC.typeOf(this[event]) === SC.T_FUNCTION) return true;
1379     if (this._registeredStringEventHandlers[event]) return true;
1380     if (this._registeredStateObserveHandlers[event]) return false;
1381 
1382     var len = this._registeredRegExpEventHandlers.length,
1383         i = 0,
1384         handler;
1385 
1386     for (; i < len; i += 1) {
1387       handler = this._registeredRegExpEventHandlers[i];
1388       if (event.match(handler.regexp)) return true;
1389     }
1390 
1391     return SC.typeOf(this['unknownEvent']) === SC.T_FUNCTION;
1392   },
1393 
1394   /**
1395     Returns the path for this state relative to the statechart's
1396     root state.
1397 
1398     The path is a dot-notation string representing the path from
1399     this state to the statechart's root state, but without including
1400     the root state in the path. For instance, if the name of this
1401     state if "foo" and the parent state's name is "bar" where bar's
1402     parent state is the root state, then the full path is "bar.foo"
1403 
1404     @type String
1405   */
1406   fullPath: function() {
1407     var root = this.getPath('statechart.rootState');
1408     if (!root) return this.get('name');
1409     return this.pathRelativeTo(root);
1410   }.property('name', 'parentState').cacheable(),
1411 
1412   toString: function() {
1413     return this.get('fullPath');
1414   },
1415 
1416   /** @private */
1417   _statechartOwnerDidChange: function() {
1418     this.notifyPropertyChange('owner');
1419   },
1420 
1421   /**
1422     Used to log a state warning message
1423   */
1424   stateLogWarning: function(msg) {
1425     var sc = this.get('statechart');
1426     sc.statechartLogWarning(msg);
1427   },
1428 
1429   /**
1430     Used to log a state error message
1431   */
1432   stateLogError: function(msg) {
1433     var sc = this.get('statechart');
1434     sc.statechartLogError(msg);
1435   }
1436 
1437 });
1438 
1439 /**
1440   Use this when you want to plug-in a state into a statechart. This is beneficial
1441   in cases where you split your statechart's states up into multiple files and
1442   don't want to fuss with the sc_require construct.
1443 
1444   Example:
1445 
1446       MyApp.statechart = SC.Statechart.create({
1447         rootState: SC.State.design({
1448           initialSubstate: 'a',
1449           a: SC.State.plugin('path.to.a.state.class'),
1450           b: SC.State.plugin('path.to.another.state.class')
1451         })
1452       });
1453 
1454   You can also supply hashes the plugin feature in order to enhance a state or
1455   implement required functionality:
1456 
1457       SomeMixin = { ... };
1458 
1459       stateA: SC.State.plugin('path.to.state', SomeMixin, { ... })
1460 
1461   @param value {String} property path to a state class
1462   @param args {Hash,...} Optional. Hash objects to be added to the created state
1463 */
1464 SC.State.plugin = function(value) {
1465   var args;
1466 
1467   // Fast arguments access.
1468   // Accessing `arguments.length` is just a Number and doesn't materialize the `arguments` object, which is costly.
1469   args = new Array(arguments.length - 1); // SC.A(arguments).shift()
1470   for (var i = 0, len = args.length; i < len; i++) { args[i] = arguments[i + 1]; }
1471 
1472   var func = function() {
1473     var klass = SC.objectForPropertyPath(value);
1474     if (!klass) {
1475       console.error('SC.State.plugin: Unable to determine path %@'.fmt(value));
1476       return undefined;
1477     }
1478     if (!klass.isClass || !klass.kindOf(SC.State)) {
1479       console.error('SC.State.plugin: Unable to extend. %@ must be a class extending from SC.State'.fmt(value));
1480       return undefined;
1481     }
1482     return klass.extend.apply(klass, args);
1483   };
1484   func.statePlugin = YES;
1485   return func;
1486 };
1487 
1488 SC.State.design = SC.State.extend;
1489