/**
 * @class ST.playable.Playable
 * This class is instantiated for each event record passed to the `{@link ST#play}`
 * method. The items in the array passed to `{@link ST#play}` are passed to the
 * `constructor` as the config object.
 */
 
ST.playable = ST.playable || {}; // setup ST.playable namespace.
 
ST.playable.Playable = ST.define({
    isPlayable: true,
 
    statics: {
        /**
         * From an playable config object (event) determine if the Playable
         * type has an fn to execute when played. Allows the Player to decide
         * whether to playFn() or inject() without instantiating the event.
         * @param event
         */
        hasFn: function (event) {
            if (typeof event.hasFn !== 'undefined') {
                return event.hasFn;
            }
            
            if (typeof event.fn !== 'undefined') {
                return event.fn;
            }
 
            var playableClsName, playableCls, has;
 
            try {
                playableClsName = event.futureClsName + '.prototype.playables.' + ST.capitalize(event.type);
                playableCls = ST.clsFromString(playableClsName);
                has = playableCls && playableCls.prototype && playableCls.prototype.fn;
            } catch (e) {
                // this happens either because:
                // a) the event type is a directly injected type like pointerdown, mousemove, etc
                // b) a real error where the type is meant to be a Playable class but the definition is missing
            }
 
            event.hasFn = !!has;
 
            return has;
        },
 
        hasLocatorFn: function (event) {
            var playableClsName, playableCls, has;
 
            try {
                playableClsName = event.futureClsName + '.prototype.playables.' + ST.capitalize(event.type);
                playableCls = ST.clsFromString(playableClsName);
                has = playableCls.prototype.locatorFn;
            } catch (e) {
                has = false;
            }
 
            return has;
        },
 
        isRemoteable: function (event) {
            if (typeof event.remoteable !== 'undefined') {
                return event.remoteable;
            }
 
            var playableClsName, playableCls, remoteable;
 
            try {
                playableClsName = event.futureClsName + '.prototype.playables.' + ST.capitalize(event.type);
                playableCls = ST.clsFromString(playableClsName);
                remoteable = playableCls.prototype.remoteable;
                if (typeof remoteable === 'undefined') {
                    remoteable = true;
                }
            } catch (e) {
                remoteable = true; // default is true
            }
 
            event.remoteable = remoteable; // cache the value for fun?
 
            return remoteable;
        }
    },
 
    /**
     * Constructor for an instance.
     * @param {Function/Number/Object} config A function is used to set the `fn` config
     * while a number sets the `delay`. Otherwise, the properties of the `config` object
     * are copied onto this instance.
     */
    constructor: function (config) {
        var me = this,
            t = typeof config;
 
        if (t === 'function') {
            me.fn = config;
            me.delay = 0;
        }
        else if (t === 'number') {
            me.delay = config;
        }
        else {
            ST.apply(me, config);
 
            if (me.delay === undefined && typeof me.fn === 'function') {
                me.delay = 0;
            }
        }
    },
 
    inject: function (done) {
        return this.context.inject(this,done);
    },
    
    callFn: function (done) {
        return this.context.callFn(this,done);
    },
 
    /**
     * For injectable events this property holds the event type. For example, "mousedown".
     * This should not be specified for non-injectable events.
     * @cfg {String} type
     */
 
    /**
     * @cfg {String/Function} target
     * A function that returns the target DOM node or {@link ST.Locator locator string}.
     */
 
    /**
     * The located element for the `target` of this event.
     * @property {ST.Element} targetEl
     * @readonly
     * @protected
     */
 
    /**
     * The browser Context in which to play this Playable. For example either Local, which
     * plays events directly into the current window/document or Webdriver, which uses
     * webdriverio calls to play events remotely via the webdriverio API.
     * @property {ST.context.Context} context
     * {@link #context}
     */
 
    /**
     * @cfg {String} relatedTarget
     * A function that returns the relatedTarget DOM node or
     * {@link ST.Locator locator string}.
     */
 
    /**
     * The located element for the `relatedTarget` of this event.
     * @property {ST.Element} relatedEl
     * @readonly
     * @protected
     */
 
    /**
     * The number of milliseconds of delay to inject after playing the previous event
     * before playing this event.
     * @cfg {Number} delay
     */
 
    /**
     * The function to call when playing this event. If this config is set, the `type`
     * property is ignored and nothing is injected.
     *
     * If this function returns a `Promise` that promise is resolved before the next
     * event is played. Otherwise, this function should complete before returning. If
     * this is not desired, the function must declare a single argument (typically named
     * "done" and call this function when processing is finished).
     *
     *      [{
     *          fn: function () {
     *              // either finish up now or return a Promise
     *          }
     *      }, {
     *          fn: function (done) {
     *              somethingAsync(function () {
     *                  // do stuff
     *
     *                  done(); // mark this event as complete
     *              });
     *          }
     *      }]
     *
     * @cfg {Function} fn
     */
 
    /**
     * @cfg {Number} x
     */
 
    /**
     * @cfg {Number} y
     */
 
    /**
     * @cfg {Number} button
     */
 
    /**
     * @cfg {Boolean} [animation=true]
     * Determines if animations must complete before this event is ready to be played.
     * Specify `null` to disable animation checks.
     */
 
    /**
     * @cfg {Boolean} [visible=true]
     * Determines if the `target` and `relatedTarget` must be visible before this event is
     * ready to be played. Specify `false` to wait for elements to be non-visible. Specify
     * `null` to disable visibility checks.
     */
 
    /**
     * @cfg {Boolean} [available=true]
     * Determines if the `target` and `relatedTarget` must be present in the dom (descendants
     * of document.body) before this event is ready to be played. Specify `false` to wait
     * for elements to be non-available. Specify `null` to disable availability checks.
     */
 
    /**
     * @cfg {Function} ready
     * An optional function that returns true when this event can be played. This config
     * will replace the `ready` method.
     */
 
    /**
     * @cfg {Number} timeout
     * The maximum time (in milliseconds) to wait for this event to be `ready`. If this
     * time is exceeded, playback will be aborted.
     */
 
    /**
     * @property {"init"/"queued"/"pending"/"playing"/"done"} state
     * @private
     */
    state: 'init',
 
    /**
     * Returns true when this event is ready to be played. This method checks for the
     * existence and visibility (based on the `visible` config) of the `target` and
     * `relatedTarget` elements. In addition, this method also waits for animations to
     * finish (based on the `animation` config).
     * @return {Boolean}
     */
    ready: function () {
        var ret = this.animationsDone() && this.targetReady() && this.targetReady(true);        
        return ret;
    },
 
    /**
     * This function is needed since a Playable/Event could have a custom ready function.
     */
    isReady: function () {
        return this.context.ready(this);
    },
 
    /**
     * Returns `true` when there are no animations in progress. This method respects the
     * `animation` config to disable this check.
     * @return {Boolean}
     */
    animationsDone: function () {
        var me = this,
            ext = window.Ext,
            anim = me.animation,
            fx, mgr;
 
        if (me.animation) {
            anim = (mgr = (fx = ext && ext.fx) && fx.Manager) && mgr.items;
 
            if (anim && anim.getCount()) {
                // TODO: sencha touch / modern toolkit flavor
                return me.setWaiting('animations', 'complete');
            }
        }
 
        return me.setWaiting(false);
    },
 
    /**
     * Returns `true` when the specified target is ready. The `ST.Element` instance is
     * cached on this object based on the specified `name` (e.g., "targetEl"). This method
     * respects the `visible` config as part of this check.
     *
     * @param {Boolean/String} [name="target"] The name of the target property. This is
     * the name of the property that holds the {@link ST.Locator locator string}. If
     * `true` or `false` are specified, these indicate the `relatedTarget` or `target`,
     * respectively.
     * @return {Boolean}
     */
    targetReady: function (name) {
        name = (name === true) ? 'relatedTarget' : (name || 'target');
 
        var me = this,
            elName = me._elNames,
            target = me[name],
            visibility = me.visible,
            availability = me.available,
            root = me.root || null,
            direction = me.direction,
            absent = availability === false,
            dom, el;
        
        ST.logger.debug('Playable.targetReady(), name='+name+', type='+me.type+', target='+target);
 
        if (target) {
            elName = elName[name] || (elName[name] = name + 'El');
 
            if (!(el = me[elName])) {
                // new case where target function must return the locator...
                if (typeof target === 'function') {
                    var t = me.target();
                    if (t && t.isPlayable) {
                        target = t;
                    }
                }
 
                if (target.isPlayable) {
                    // When a sequence of events targets the same thing, we can queue it
                    // like so:
                    //
                    //      ST.play([
                    //          { target: '@foo', ... },
                    //          { target: -1, ... },
                    //          { target: -2, ... }
                    //      ]);
                    //
                    // Which gets enqueued like this:
                    //
                    //      Q[0] = new ST.playable.Playable({ target: '@foo', ... });
                    //      Q[1] = new ST.playable.Playable({ target: Q[0], ... });
                    //      Q[2] = new ST.playable.Playable({ target: Q[0], ... });
                    //
                    me[elName] = el = target[elName];
                }
            }
 
            ST.logger.debug('Playable.targetReady(), type='+me.type+', el='+el);
 
            if (el) {
                if (absent) {
                    if (el.isDetached()) {
                        return me.setWaiting(name, 'absent');
                    }
                    return me.setWaiting(false);
                }
 
                if (typeof target === 'string') {
                    target.$scope = me; // attach the Playable as the scope for any target functions.
                    dom = ST.find(target, false, root, direction);
 
                    ST.logger.debug('Playable.targetReady(), el, target is string, dom=',dom);
 
                    if (dom && el.dom !== dom) {
                        // We store the wrapped el as soon as we find it, but if we are
                        // waiting for visibility it may be that the locator will not
                        // match the same DOM node from the previous tick. Instead of
                        // creating a new ST.Element we simply reset the dom property.
                        // Because we have a target string (vs a target playable), the
                        // ST.Element we stored is our own.
                        el.dom = dom;
                    }
                }
            } else {
                if (target.isPlayable) { 
                    el = target[elName]; 
                    dom = el && el.dom;
                } else {
                    if (typeof target === 'function') {
                        // if this is a function, poke on the correct scope
                        target.$scope = me;
                    }
                    dom = ST.find(target, false, root, direction);
                }
 
                ST.logger.debug('Playable.targetReady(), no el, not playable, dom=',dom);
                if (dom) {
                    me[elName] = el = new ST.Element(dom);
 
                    if (absent) {
                        return me.setWaiting(name, 'absent');
                    }
                }
                else if (absent) {
                    return me.setWaiting(false);
                }
                else if (availability !== null) {
                    // availability is seldom ever true... it is false if we want to
                    // wait for the DOM node to be unavailable (which was handled above)
                    // so that leaves availability === null which indicates we should
                    // not wait.
                    return me.setWaiting(name, 'available');
                }
            }
 
            if (el && visibility !== null) {
                if (visibility === false) {
                    if (el.isVisible()) {
                        return me.setWaiting(name, 'not visible');
                    }
                } else if (!el.isVisible()) {
                    return me.setWaiting(name, 'visible');
                }
            }
        }
 
        ST.logger.debug('Playable.targetReady(), SET WAITING FALSE, name='+name+', type='+me.type+', target='+target+', el='+el);
 
        return me.setWaiting(false);
    },
 
    /**
     * This string contains the name of the item preventing readiness of this event. For
     * example, "target" or "relatedTarget". This is used to formulate an appropriate
     * error message should the `timeout` be exceeded. See `setWaiting` for setting
     * this value.
     * @property {String} waitingFor
     * @readonly
     * @protected
     */
 
    /**
     * This string describes the aspect of the item preventing readiness of this event.
     * For example, "available" or "visible". This is used to formulate an appropriate
     * error message should the `timeout` be exceeded. See `setWaiting` for setting
     * this value.
     * @property {String} waitingState
     * @readonly
     * @protected
     */
 
    /**
     * Updates the `waitingFor` and `waitingState` properties given their provided values
     * and returns `true` if this call clears the `waitingFor` property.
     *
     * This method is not normally called by user code but should be called if a custom
     * `ready` method is provided to ensure timeouts have helpful state information.
     *
     * @param {Boolean/String} waitingFor The {@link #waitingFor} value or `false` to clear
     * the waiting state.
     * @param {String} waitingState The {@link #waitingState} value.
     * @return {Boolean} `true` if `waitingFor` is `false` and `false` otherwise.
     * @protected
     */
    setWaiting: function (waitingFor, waitingState) {
        if (waitingFor === false) {
            waitingFor = waitingState = null;
        }
 
        this.waitingFor = waitingFor;
        this.waitingState = waitingState;
 
        return !waitingFor;
    },
 
    /**
     * The timestamp recorded when the event is first checked for readiness and found
     * not `ready`.
     * @property {Number} waitStartTime
     * @private
     * @readonly
     */
    waitStartTime: 0,
 
    _elNames: {
        relatedTarget: 'relatedEl'
    },
 
    getDom: function () {
        var el = this.getElement();
        return el && el.dom;
    },
 
    getElement: function () {
        return this.future && this.future.el;
    },
 
    getComponent: function () {
        return this.future && this.future.cmp;
    },
 
    getInstanceData: function (key) {
        var data = this.instanceData;
 
        if (data && key) {
            return data[key];
        }
 
        return data;
    },
 
    getFutureData: function (key) {
        var data = this.future.data;
 
        if (data && key) {
            data = data[key];
        }
 
        return data;
    },
 
    setFutureData: function (config) {
        var data = this.future.data;
 
        ST.apply(data,config);
    },
 
    initInstanceData: function () {
        this.instanceData = this.instanceData || {};
    },
 
    initFutureData: function () {
        this.future.data = this.future.data || {};
    },
 
    updateEl: function (dom) {
        var me = this,
            el = me.future.el;
 
        if (!dom) {
            me.future.el = null;
        } else if (!el || el.dom !== dom) {
            me.future.el = ST.get(dom);
        }
 
        me.future.locator.targetEl = me.future.el;
        
        if (me.future._attach) {
            me.future._attach();
        }
    }
});
 
 
ST.playable.define = function (playableName, body) {
    if (!body.extend) {
        body.extend = ST.playable.Playable;
    }
 
    var cls = ST.define(body),
        parts = playableName.split('.'),
        classScope = ST.playable,
        name;
 
    while (parts.length > 1) {
        name = parts.shift();
 
        if (!classScope[name]) {
            classScope[name] = {};
        }
    }
 
    name = parts[0];
 
    return classScope[ST.capitalize(name)] = cls;
};