(function () {
var logger = ST.logger.forClass('event/Player');
 
ST.event = ST.event || {}; // setup ST.event namespace.
 
/**
 * @class ST.event.Player
 * @extend ST.event.Driver
 *
 * This class is rarely used directly, but is the underlying mechanism used the inject
 * events using the {@link ST#play} and {@link ST.future.Element futures} API's.
 */
ST.event.Player = ST.define({
    extend: ST.event.Driver,
 
    /**
     * @cfg {Array} events The event queue to playback. This must be provided before
     * the {@link #method-start} method is called.
     */
 
    /**
     * @cfg {Boolean} pauseForAnimations True to pause event playback during animations, false
     * to ignore animations. Default is true.
     */
    pauseForAnimations: true,
 
    /**
     * @cfg {Boolean} eventTranslation
     * `false` to disable event translation.  If `false` events that are not supported by
     * the browser's event APIs will simply be skipped.
     * NOTE: this is passed down to the browser context object.
     */
    eventTranslation: true,
 
    /**
     * @cfg {Boolean} visualFeedback
     * `false` to disable visual feedback during event playback (mouse cursor, and "gesture"
     * indicator)
     * NOTE: this is passed down to the browser context object.
     */
    visualFeedback: true,
 
    /**
     * @cfg {Number} timeout
     * The event player will wait for certain conditions to be met before playing each
     * event in the queue.  These conditions include:
     *
     * - target element present and visible in DOM
     * - Ext JS framework animations complete
     *
     * If the time waited for these conditions to be met exceeds this timeout value,
     * playback will be aborted.
     *
     * If specified, this will be the default {@link ST.playable.Playable#timeout timeout}
     * for all events. Otherwise, the {@link ST.options#timeout timeout} will be
     * used.
     */
 
    /**
     * @cfg {Number} waitInterval
     * Amount of time (in milliseconds) to wait before trying again if event cannot be
     * played due to target element not being available, animations in progress, etc.
     */
    waitInterval: ST.isIE9m ? 50 : 10,
    
    paused: 0,
 
    /**
     * This property holds the event last pulled from the queue but yet to be played.
     */
    pendingEvent: null,
 
    pendingFn: false,
 
    /**
     * @event timeout
     * Fires when the player times out while waiting for conditions to be met in order
     * for playback to proceed, for example, target element unavailable or animations
     * never finished.
     * @param {ST.event.Player} this
     * @param {String} message A message containing the reason for timeout
     */
 
    constructor: function (config) {
        var me = this;
 
        me.events = [];
        ST.apply(me, config);
 
        if (!me.context) {
            me.context = ST.defaultContext;
        }
 
        me.context.player = me;
 
        me._nextId = 1;
    },
 
    /**
     * Adds new {@link ST.playable.Playable events} to the play queue.
     *
     * This method is not normally called by user code. Use {@link ST#play} insteaad.
     * @param [index]
     * @param {ST.playable.Playable[]} events The events to add or config objects for
     * them.
     * @return {ST.playable.Playable[]} The `events` array with each element now promoted
     * from config object to `ST.playable.Playable` instance.
     * @private
     */
    add: function (index, events) {
        var me = this,
            _events = me.events,
            length = _events.length,
            timeout = ST.options.timeout,
            count, inserting, e, i, queue, t;
 
        // ST.logger.debug('Player.add(index=',index,', events=',events,', me.events=',me.events);
 
        if (typeof index !== 'number') {
            events = index;
            index = null;
        }
 
        count = events.length;
 
        // The default behavior is to insert at the beginning of the queue if there is
        // a "playable" fn further up the call stack.  This is primarily to accomodate
        // nested future calls, for example:
        //
        // ST.element('foo').and(function() {
        //     ST.element('bar').and(function() {
        //         foo.innerHTML = 'bar is available';
        //     });
        // }).click();
        //
        // In the above example the events should be played in the order they appear visually.
        // The "click" event should happen after foo's innerHTML is changed.
        if (index == null) {
            // This value is set to 0 by and() before it calls the user's fn.
            index = me.pendingFn;
 
            // And to "false" when we are not inside an and() callback.
            if (index === false) {
                index = length;
            } else {
                // Each call to a futures method enqueues events here. If these are
                // made sequentially in an and() callback, we need to preserve their
                // order by tracking where we left off from the last call.
                me.pendingFn += count;
            }
        }
 
        inserting = index < length;
 
        // splice() is bugged on IE8 so we avoid it and just slice the events prior to
        // "index" into a new array.
        queue = inserting ? (index ? _events.slice(0, index) : []) : _events;
 
        for (i = 0; i < count; ++i) {
            e = events[i];
 
            if (e || e === 0) {  // if (not truthy or 0 skip it)
                var context = e.context || me.context;
 
                events[i] = e = context.initEvent(e);
 
                if (e.typingDelay === undefined) {
                    e.typingDelay = me.typingDelay;
                }
                if (e.visualFeedback === undefined) {
                    e.visualFeedback = me.visualFeedback;
                }
                if (e.eventTranslation === undefined) {
                    e.eventTranslation = me.eventTranslation;
                }
 
                // Check for { target: -1, ... } and promote it to use the playable
                // backwards that number from this entry.
                //
                if (typeof(t = e.target) === 'number') {
                    e.target = queue[queue.length + t];
                } else if (t && t.dom) {
                    e.targetEl = t.$ST ? t : new ST.Element(t.dom);
                }
 
                if (typeof(t = e.relatedTarget) === 'number') {
                    e.relatedTarget = queue[queue.length + t];
                } else if (t && t.dom) {
                    e.relatedTargetEl = t.$ST ? t : new ST.Element(t.dom);
                }
 
                // Only use the default eventDelay for actual "events".  Other things
                // in the queue (like functions added by futures) should run as soon
                // as possible
                if (e.delay === undefined && (!e.future) && e.type !== 'wait') {
                    e.delay = me.eventDelay;
                }
                if (e.animation === undefined) {
                    e.animation = me.pauseForAnimations;
                }
 
                if (!timeout) {
                    // If ST.options.timeout is 0, ignore all timeouts
                    e.timeout = 0;
                } else if (e.timeout === undefined) {
                    if ((e.timeout = me.timeout) === undefined) {
                        e.timeout = timeout;
                    }
                }
 
                e._player = me;
                e.id = me._nextId++;
                e.state = 'queued';
 
                queue.push(e);
            }
        }
 
        if (inserting) {
            // finally we push the events at and beyond "index" onto the new array and
            // update me.events.
            for (; index < length; ++index) {
                queue.push(_events[index]);
            }
            me.events = queue;
        }
 
        if (me.active && !me.pendingEvent) {
            // If there is a pendingEvent then playNext will eventually be called. We
            // need to call it otherwise, but we must not call it if there is an event
            // pending as events could be played out of order (each trying to play at
            // the same time).
            me.playNext();
        }
    },
 
    cleanup: function () {
        var me = this,
            eventTimer = me.eventTimer,
            context = me.context;
 
        me.pendingEvent = null;
        me.pendingFn = false;
 
        if (eventTimer) {
            me.eventTimer = 0;
            ST.deferCancel(eventTimer);
        }
 
        me.events.length = 0;
 
        if (context) {
            context.cleanup();
        }
    },
 
    skip: function () {
        var me = this;
        me.pendingEvent = null;
        me.pendingFn = false;
        me.playNext();
    },
 
    onStart: function () {
        this.playNext();
    },
 
    onStop: function () {
        var me = this,
            context = me.context;
 
        me.cleanup();
 
        if (context) {
            context.stop();
        }
    },
 
    pause: function () {
        var me = this,
            eventTimer = me.eventTimer,
            event = me.pendingEvent;
 
        ++me.paused;
 
        if (eventTimer) {
            me.eventTimer = 0;
            ST.deferCancel(eventTimer);
        }
 
        if (event && !me.pendingFn) {
            // Put back the pending event unless we are called from its "fn".
            me.events.unshift(event);
            event.state = 'queued';
        }
    },
 
    resume: function () {
        var me = this;
 
        if (me.paused) {
            if (! --me.paused) {
                me.playNext();
            }
        }
    },
 
    isPlaying: function () {
        return (this.events.length > 0 || this.pendingEvent);
    },
 
    playNext: function () {
        var me = this,
            events = me.events,
            event;
 
        if (!me.pendingEvent && !me.paused) {
            event = events.shift();
 
            if (!event) {
                me.onEnd();
            } else {
                if (me.pendingFn) {
                    me.pendingFn--;
                }
                me.pendingEvent = event;
                event.state = 'pending';
                me.playEventSoon(event, event.delay || 0);
            }
        }
    },
 
    onEnd: function () {
        var me = this,
            context = me.context;
 
        context.onEnd(function () {
            me.fireEvent('end', me);
        }, function (err) {
            console.log(err); // eslint-disable-line
        });
    },
 
    playFn: function (event) {
        logger.debug('Player.playFn, event.type='+event.type+', event.timeout='+event.timeout);
        var me = this,
            context = event.context,
            // user function can either invoke our "done" function when complete
            // or return a promise.  We will not continue to play events until
            // either the done function is invoked or the promise is resolved.
            watchDog = new ST.WatchDog(function(err) {
                logger.debug('watchdog done called, event.type='+event.type);
                logger.debug('watchdog done called, failed='+failed+', err='+err);
                if (err) {
                    // if there is waitingFor/waitingState information then timeout so 
                    // we present that information instead of the more generic message
                    if (event.waitingFor && event.waitingState) {
                        me.doTimeout(event);
                    } else {
                        me.doError(err);
                    }
                } else {
                    me.pendingEvent = null;
                    event.state = 'done';
                    me.playNext();
                }
            }, event.timeout),
            done = watchDog.done,
            failed, promise;
 
            event.watchdogStarted = new Date().getTime(); // for use in State timer
 
        me.pendingFn = 0;
 
        if (ST.options.handleExceptions) {
            try {
                promise = context.callFn(event, done);
            } catch (e) {
                logger.debug('error with context.callFn, e='+e);
                done.fail(e); // e is sent to watchDog(callback) function above
                return;
            }
        } else {
            promise = context.callFn(event, done);
        }
 
        me.pendingFn = false;
 
        if (promise && typeof promise.then === 'function') {
            // Return value is "then-able" so it qualifies as a Promise.
            promise.then(function () {
                done();
            }, function (err) {
                done.fail(err); // err is sent to watchDog(callback) function above
            });
        }
        else if (!event.fn.length) {
            // Check the arity (length) of the Function. If it is 0, the function
            // does not declare any arguments so assume it has completed.
            done();
        }
    },
 
    playEvent: function (event) {
        logger.debug('ST.play(event), event.type='+event.type + ', event.target='+event.target);
 
        var me = this,
            now = +new Date(),
            waitStartTime = event.waitStartTime,
            timeout = event.timeout,
            context = event.context;
 
        var notReadyFn = function (err) {
            logger.debug('playEvent, notReadyFn called, event.type='+event.type + ', err=',err);
 
            if (err) {
                me.doError(err);
                return; // don't play the event again, we're done.
            }
 
            if (waitStartTime && timeout && ((now - waitStartTime) >= timeout)) {
                me.doTimeout(event);
            } else {
                // Waiting for some condition to be met before we can proceed
                // such as target element to become available or animations to complete
                // Try again after a brief delay.
 
                if (!waitStartTime) {
                    // a timestamp as of the time we began waiting (for timeout purposes)
                    event.waitStartTime = +new Date();
                }
 
                me.playEventSoon(event);
            }
        };
        
        context.ready(event,
            function (ret) {
                logger.debug('Player ready, resolve, ret='+ret);
 
                event.state = 'playing';
                if (ST.playable.Playable.hasFn(event) || (event.fn && typeof event.fn === 'function')) {
                    me.playFn(event);
                } else {
                    me.pendingEvent = null;
                    try {
                        context.inject(event, function () {
                            event.state = 'done';
                            me.playNext();
                        }, function (err) {
                            notReadyFn(err);
                        });
                    } catch (e) {
                        notReadyFn(e);
                    }
                }
            }, notReadyFn);
    },
 
    playEventSoon: function (playable, delay) {
        var me = this;
 
        me.eventTimer = ST.defer(function () {
            me.eventTimer = 0;
            me.playEvent(playable);
        }, (delay == null) ? me.waitInterval : delay);
    },
 
    fail: function(message) {
        var me = this;
 
        // When an error occurs we empty all remaining events from the queue.  This
        // does not "stop" the player - if additional events are added they will be
        // automatically played.
        me.cleanup();
 
        if (me.context.isRecording) {
            ST.sendMessage({
                type: 'systemError',
                message: message + '. Fix the error and try recording again.'
            });
        }
 
        me.fireEvent('error', {
            player: me,
            message: message
        });
 
        me.onEnd();
    },
 
    doError: function (err) {
        var message = (err && err.message) || err;
        this.fail('Failed with error "' + message + '"');
    },
 
    doTimeout: function (event) {
        if (event.state !== 'pending' && event.state !== 'playing') {
            return;
        }
        event.state = 'done';
 
        var s = event.waitingFor || 'event',
            locator = event[s],
            toBe = event.waitingState || 'ready',
            futureClsName = event.futureClsName,
            src = event,
            timedout = true,
            dom, sel, futureTarget;
 
        if (s === 'target' || s === 'relatedTarget') {
            while (src) {
                if (!(locator = src[s])) { // first check "target"
                    // we might have a targetEl, so use its id or tagName
                    locator = src[s + 'El'];
                    dom = locator && locator.dom;
                    break;
                }
 
                if (locator.isPlayable) {
                    src = locator;  // traverse to the target playable for location
                } else {
                    if (locator.dom) {
                        dom = locator.dom;
                    } else if (typeof locator.nodeType === 'number') {
                        dom = locator;
                    } else {
                        sel = locator.toString(); // handle strings & functions
                    }
                    break;
                }
            }
 
            if (dom) {
                // we have a targetEl, so use its id or tagName
                sel = dom.id;
                sel = sel ? '#' + sel : dom.tagName;
            }
            if (sel) {
                s += ' (' + sel + ')';
            }
        } else if (src.future.locator.target) {
            sel = src.future.locator.target;
            futureTarget = true;
        }
 
        if (futureClsName) {
            toBe += ' for ' + event.futureClsName;
        }
 
        if (futureTarget) {
            toBe += ' with target ' + sel;
        }
 
        if (event.expectTimeout) {
            ST.status.addResult({
                passed: true,
                message: 'expected timeout waiting for ' + s + ' to be ' + toBe
            });
            var me = this;
            if (!me.events.length) {
                me.cleanup();
                me.onEnd();
                return;
            } else {
                timedout = false;
            }
        }
 
        if (timedout) {
            this.fail('Timeout waiting for ' + s + ' to be ' + toBe);
        } else {
            me.skip();
        }
    }
});
}());