Source: core/state-chart.js

/**
 * @module montage/core/state-chart
 * @requires montage
 */
var Montage = require("./core").Montage;

/**
 * @class State
 * @extends Montage
 */
var State = exports.State = Montage.specialize( /** @lends State# */{

    _stateChart: {
        enumerable: false,
        value: null
    },

    /**
     * Initializes a State object with a set of options.
     * @function
     * @param {string} options The options for the new State.
     * @returns {State}
     */
    init: {
        value: function (options) {

            this.substates = {};
            this.enterState = null;
            this.exitState = null;

            var keys = Object.keys(options),
                i = 0,
                iKey,
                iOption;

            for (; (iKey = keys[i]); i++) {

                iOption = options[iKey];
                if (typeof iOption === "object" && State.prototype.isPrototypeOf(iOption)) {

                    iOption.name = iKey;
                    iOption.parentState = this;
                    this.substates[iKey] = iOption;
                }

                // TODO is it worth guarding against any old property being added to this state?
                if (typeof iOption === "string" && "initialSubstate" !== iKey) {
                    // this is a simple gotoState transition syntax
                    this[iKey] = this._encloseGotoState(iOption);
                } else {
                    this[iKey] = iOption;
                }
            }

            return this;
        }
    },

    /**
     * The name of the state.
     * @type {Property}
     * @default {string} null
     */
    name: {
        enumerable: false,
        value: null
    },

    _initialSubstate: {
        enumerable: false,
        value: null
    },

    /**
     * The intitial substate.
     * @type {Function}
     * @default {string} null
     */
    initialSubstate: {
        get: function () {
            if (typeof this._initialSubstate === "string") {
                this._initialSubstate = this[this._initialSubstate];
            }
            return this._initialSubstate;
        },
        set: function (value) {
            this._initialSubstate = value;
        }
    },

    /**
     * The set of substates.
     * @type {Property}
     * @default {string} null
    */
    substates: {
        enumerable: false,
        value: null
    },

    /**
     * The state's parent state.
     * @type {Property}
     * @default {string} null
     */
    parentState: {
        enumerable: false,
        value: null
    },

    _path: {
        enumerable: false,
        value: null
    },

    /**
     * @type {Function}
     * @default {string} null
     */
    path: {
        // TODO add dependency on parentState, remember to clear cached value
        enumerable: false,
        get: function () {
            if (!this._path) {

                if (this.parentState && this.parentState.path) {
                    this._path = this.parentState.path + "." + this.name;
                } else {
                    this._path = this.name;
                }
            }
            return this._path;
        }
    },

    /**
     * @type {Property}
     * @default {string} null
     */
    enterState: {
        enumerable: false,
        value: null
    },

    /**
     * @type {Property}
     * @default {string} null
     */
    exitState: {
        enumerable: false,
        value: null
    },

    /**
     * @function
     * @param {string} otherState
     * @returns `!!this.path.match(new RegExp(".?" + otherState + ".?"))`
     */
    // TODO XXX @returns block should be in code
    isInState: {
        enumerable: false,
        value: function (otherState) {

            if (typeof otherState !== "string") {
                otherState = otherState.name;
            }

            return !!this.path.match(new RegExp(".?" + otherState + ".?"));
        }
    },

    _encloseGotoState: {
        value: function (state) {
            return function (stateChart, owner) {
                // Not relying on original implementation to save an extra function call, despite duplicated code
                return this._stateChart._gotoState(state, owner);
            };
        }
    },

    /**
     * @function
     * @param {Property} state
     * @param {Property} owner
     * @returns this._stateChart._gotoState(state, owner)
     */
    gotoState: {
        value: function (state, owner) {
            return this._stateChart._gotoState(state, owner);
        }
    },

    _performAction: {
        enumerable: null,
        value: function (actionName, stateChart, owner) {
            if (this[actionName]) {
                // TODO what should the context be inside the action function:
                // state or stateChart?  state makes sense but requires that
                // authors know that when building a stateChart it's easy to
                // get annoyed with having to remember which one you're in when
                // building a relatively complex stateChart
                // we could just .call(this._stateChart, args) etc...
                this[actionName](stateChart, owner);

            } else if (this.parentState) {
                this.parentState._performAction(actionName, stateChart, owner);
            } else {
                throw "Action '" + actionName + "' not available";
            }
        }
    },

    /**
     * @function
     * @returns "[State " + this.path + " ]"
     */
    toString: {
        enumerable: false,
        value: function () {
            return "[State " + this.path + " ]";
        }
    }

});

/**
 * @class StateChart
 */
var StateChart = exports.StateChart = Montage.specialize(/** @lends StateChart# */ {

    /**
     * @type {Property}
     * @default {string} null
     */
    delegate: {
        enumerable: false,
        value: null
    },

    // When the ownerStateProperty is set, we expect all operations of this
    // stateChart to rely on the owner property.
    // That is, many objects can rely on this stateChart to manage the flow of
    // state but keep track of their own currentState (exposed as
    // ownerStateProperty and recorded at "_" + ownerStateProperty);
    // When this is set, the statechart itself is completely stateless and an
    // object must take ownership of it prior to performing any actions

    /**
     * @type {Property}
     * @default {string} null
     */
    ownerStateProperty: {
        enumerable: false,
        value: null
    },

    /**
     * @type {Property}
     * @default {string} null
     */
    rootState: {
        enumerable: false,
        value: null
    },

    _currentState: {
        enumerable: false,
        value: null
    },

    /**
     * The current state.
     * @function
     * @returns The current state.
     */
    currentState: {
        get: function () {
            return this.ownerStateProperty ? null : this._currentState;
        }
    },

    /**
     * Initializes a StateChart with a State object, and returns the
     * StateChart.
     * @function
     * @param {string} state TODO
     * @returns {StateChart}
    */
    initWithState: {
        value: function (state) {

            this._states = {};

            this.rootState = state;
            this.rootState._stateChart = this;
            this._prepareState(this.rootState);

            this.enterDefaultState();

            return this;
        }
    },

    _defaultState: {
        enumerable: false,
        value: null
    },

    /**
     * The default state.
     * @function
     * @returns this._defaultState
     */
    defaultState: {
        enumerable: false,
        get: function () {
            if (!this._defaultState) {

                var deepestState, nextState;
                deepestState = nextState = this.rootState;

                while ((nextState = nextState.initialSubstate)) {
                    deepestState = nextState;
                }

                this._defaultState = deepestState;
            }

            return this._defaultState;
        }
    },

    /**
     * @function
     * @returns this.defaultState
     */
    enterDefaultState: {
        enumerable: false,
        value: function () {
            if (this.ownerStateProperty && !this.owner) {
                throw "This stateChart has been configured to require an owner to execute this function";
            }

            var owner = this.ownerStateProperty ? this.owner : this,
                // Using internal ownerStateProperty normally reserved for
            // writing, seeing as many components trigger this
            // enterDefaultState, when their ownerStateProperty.get is invoked
            // the first time. So we need to stop the infinite loop
                currentState = this.ownerStateProperty ? owner["_" + this.ownerStateProperty] : owner.currentState;

            if (currentState) {
                throw "Cannot enter default state from '" + currentState.name + "'";
            }

            // We could probably use gotoState for this but I want to minimize
            // the callbacks as we're not really transitioning for the first
            // setup of the state we're just getting there, I don't think we
            // need to make it look like a "transition" to the delegate
            var deepestState, nextState;
            deepestState = nextState = this.rootState;

            while ((nextState = nextState.initialSubstate)) {

                if (deepestState.enterState) {
                    deepestState.enterState(this, owner);
                }

                deepestState = nextState;

                if (nextState.initialSubstate && deepestState.exitState) {
                    deepestState.exitState(this, owner);
                }

            }

            if (this.ownerStateProperty) {
                owner["_" + this.ownerStateProperty] = this.defaultState;
            } else {
                this._currentState = this.defaultState;
            }

            return this.defaultState;
        }
    },

    _prepareState: {
        enumerable: false,
        value: function (state) {
            state._stateChart = this;

            // Keep a record of all states other than the root
            if (state.name) {
                this._states[state.name] = state;
            }

            var substateName;
            for (substateName in state.substates) {
                if (state.substates.hasOwnProperty(substateName)) {
                    this._prepareState(state.substates[substateName]);   
                }
            }
        }
    },

    _states: {
        enumerable: false,
        value: null
    },

    /**
     * @function
     * @param {Property} stateName TODO
     * @returns {Array} this._states[stateName]
     */
    stateWithName: {
        enumerable: false,
        value: function (stateName) {
            return this._states[stateName];
        }
    },

    /**
     * @function
     * @param {string} action TODO
     * @param {string} owner TODO
     */
    performAction: {
        value: function (action, owner) {

            if (this.ownerStateProperty && !owner) {
                throw "This stateChart has been configured to require an owner to execute this function";
            }

            owner = this.ownerStateProperty ? owner : this;

            var currentState = this.ownerStateProperty ? owner[this.ownerStateProperty] : owner.currentState;

            if (!currentState) {
                throw "Cannot perform action '" + action + "' without a currentState";
            }

            currentState._performAction(action, this, owner);

            // After performing the action, possibly with state transitions, clear out the owner
            this.owner = null;
        }
    },

    _gotoState: {
        value: function (state, owner) {

            if (this.ownerStateProperty && !owner) {
                throw "This stateChart has been configured to require an owner to execute this function";
            }

            owner = this.ownerStateProperty ? owner : this;

            var fromState = this.ownerStateProperty ? owner[this.ownerStateProperty] : owner.currentState,
                fromStateName = fromState.name,
                stateName = state,
                currentPath,
                destinationPath,
                i,
                interiorDestinationPath,
                interiorDestinationPathCount,
                lastCommonIndex,
                nextIndex,
                destinationPathCount,
                searchCount,
                iState,
                oldState,
                delegateWillExit = false,
                delegateWillEnter = false,
                delegateDidExit = false,
                delegateDidEnter = false;

            if (typeof stateName === "string") {
                state = this._states[state];
            } else {
                stateName = state.name;
            }

            //same state
            if (stateName === fromStateName) {
                return;
            }

            if (this.delegate) {
                delegateWillExit = typeof this.delegate.stateChartWillExitState === "function";
                delegateWillEnter = typeof this.delegate.stateChartWillEnterState === "function";
                delegateDidExit = typeof this.delegate.stateChartDidExitState === "function";
                delegateDidEnter = typeof this.delegate.stateChartDidEnterState === "function";
            }

            if (this.delegate && typeof this.delegate.stateChartShouldGoFromStateToState === "function") {
                if (!this.delegate.stateChartShouldGoFromStateToState(this, fromState, state)) {
                    return;
                }
            }

            if (this.delegate && typeof this.delegate.stateChartWillGoFromStateToState === "function") {
                this.delegate.stateChartWillGoFromStateToState(this, fromState, state);
            }

            currentPath = fromState.path;
            destinationPath = state.path;

            //state is inside this state
            if ((new RegExp(currentPath)).test(destinationPath)) {

                interiorDestinationPath = destinationPath.replace(new RegExp(currentPath + ".?"), "").split(".");
                interiorDestinationPathCount = interiorDestinationPath.length;
                i = 0;

                for (; i < interiorDestinationPathCount; i++) {
                    iState = this._states[interiorDestinationPath[i]];

                    if (delegateWillEnter) {
                        this.delegate.stateChartWillEnterState(this, iState);
                    }

                    if (typeof iState.enterState === "function") {
                        iState.enterState(this, owner);
                    }

                    if (delegateDidEnter) {
                        this.delegate.stateChartDidEnterState(this, iState);
                    }
                }

            }
            // state is outside this state, need to find where they fork
            else {
                currentPath = currentPath.split(".");
                destinationPath = destinationPath.split(".");

                lastCommonIndex = -1;
                destinationPathCount = destinationPath.length;
                searchCount = Math.min(currentPath.length, destinationPathCount);

                while (lastCommonIndex < searchCount) {
                    nextIndex = lastCommonIndex + 1;
                    if (currentPath[nextIndex] !== destinationPath[nextIndex]) {
                        break;
                    }
                    lastCommonIndex++;
                }

                // exit from the currentState to just before the common path
                for (i = currentPath.length - 1; i > lastCommonIndex; i--) {
                    iState = this._states[currentPath[i]];

                    if (delegateWillExit) {
                        this.delegate.stateChartWillExitState(this, iState);
                    }

                    if (typeof iState.exitState === "function") {
                        iState.exitState(this, owner);
                    }

                    if (delegateDidExit) {
                        this.delegate.stateChartDidExitState(this, iState);
                    }
                }

                // We don't want to enter the root state
                lastCommonIndex = lastCommonIndex < 0 ? 0 : lastCommonIndex;

                // enter from the common path to the new state at the end
                for (i = lastCommonIndex; i < destinationPathCount; i++) {
                    iState = this._states[destinationPath[i]];

                    if (delegateWillEnter) {
                        this.delegate.stateChartWillEnterState(this, iState);
                    }

                    // Update the currentState as we enter each state
                    // TODO is this really necessary, is it best done at this
                    // point? what about exit?
                    if (this.ownerStateProperty) {
                        owner["_" + this.ownerStateProperty] = iState;
                    } else {
                        this._currentState = iState;
                    }

                    if (typeof iState.enterState === "function") {
                        iState.enterState(this, owner);
                    }

                    if (delegateDidEnter) {
                        this.delegate.stateChartDidEnterState(this, iState);
                    }
                }
            }

            oldState = fromState;

            if (this.delegate && typeof this.delegate.stateChartDidGoFromStateToState === "function") {
                this.delegate.stateChartDidGoFromStateToState(this, oldState, state);
            }

            if (typeof owner.transitionedFromStateToState === "function") {
                owner.transitionedFromStateToState(this, oldState, state);
            }

        }
    }

});