ST.context = {}; // namespace 
 
ST._screenshotCount = 0;
 
ST.context.Base = ST.define({
 
    /**
     * @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: inherited from Player
     */
    eventTranslation: true,
 
    /**
     * @cfg {Boolean} visualFeedback 
     * `false` to disable visual feedback during event playback (mouse cursor, and "gesture"
     * indicator)
     * NOTE: inherited from Player
     */
    visualFeedback: true,
 
    /**
     * @param {Array} playables Either a config for a playable or a Playable
     * @param done
     */
    play: function (playables, done) {
        var me = this,
            player = ST.player(),
            options = ST.options,
            tail;
 
        if (done) {
            tail = {
                type: 'tail',
                remoteable: false,
                fn: function () {
                    player.un('error', tail);
 
                    if (done) {
                        done();
                    }
                }
            };
            playables.push(tail);
        }
 
        for( var i=0; i<playables.length; i++) {
            var p = playables[i];
            p.context = this;
            if (typeof p.futureClsName === 'undefined') {
                p.futureClsName = 'ST.future.Element';
            }
            p.args = p.args || {};
        }
 
        me.eventDelay = player.eventDelay = options.eventDelay;
        me.typingDelay = player.typingDelay = options.typingDelay;
        me.visualFeedback = player.visualFeedback = options.visualFeedback;
        me.eventTranslation = player.eventTranslation = options.eventTranslation;
        if (me.injector) {
            me.injector.translate = options.eventTranslation;
        }
 
        player.add(playables);
 
        if (done) {
            playables.pop();
            player.on('error', tail.fn);
        }
 
 
        player.start();  // does nothing if already started 
        return playables;
    },
    
    screenshot: function (name, tolerance, done) {
        var me = this;
        name = name || ST._screenshotCount++;
        tolerance = tolerance || 0;
        
        me._screenshot(name, function (err, comparison) {
            var expectation,
                passed;
            
            if (err) {
                expectation = {
                    passed: false,
                    message: err.stack || err.message || err
                };
            } else if (comparison) {
                passed = comparison.diffCount <= tolerance;
                expectation = {
                    passed: passed,
                    message: 'Expected screenshot ' + name + ' to match baseline.',
                    screenshot: comparison.path,
                    baseline: comparison.baseline,
                    diff: comparison.diff
                };
            } else {
                expectation = {
                    passed: true,
                    message: 'Screenshot comparison unsupported for this session'
                };
            }
            
            ST.status.addResult(expectation);
            done(comparison);
        });
    },
    
    _createInstance: function (className, config) {
        var fn = ST.clsFromString(className);
 
        if (!fn) {
            if (ST.LOGME) console.log('WARNING: no class for className='+className);
            fn = ST.playable.Playable;
        }
 
        return new fn(config);
    },
 
 
    createPlayable: function (event) {
        var me = this,
            type = event.type,
            playableClsName, playable;
 
        playableClsName = event.futureClsName + '.prototype.playables.' + ST.capitalize(type);
        playable = me._createInstance(playableClsName, event);
 
        // allows for custom events such as the expandX methods for tap/type... 
        if (!playable) {
            playable = new ST.playable.Playable(event);
        }
 
        playable.context = playable.context || me;
 
        if (ST.LOGME) console.log('Base.createPlayable() => '+playable);
 
        return playable;
    },
 
    checkGlobalLeaks: function (done) {
        var me = this,
            results;
 
        if (ST.LOGME) console.log('Base.checkGlobalLeaks()');
 
        me._checkGlobalLeaks(function (result) {
            var results = result.results,
                addedGlobals = result.addedGlobals;
 
            if (results && results.length) {
                for (var i=0; i<results.length; i++) {
                    ST.status.addResult(results[i]);
                }
            }
 
            if (addedGlobals && addedGlobals.length) {
                ST.addGlobals(addedGlobals);
            }
            done();
        });
    }
});
      }
            }
        }   
 
        if (data.statics) {
            // These will overwrite any inherited statics (as they should) 
            ST.apply(ctor, data.statics);
 
            delete data.statics;
        }
 
        ST.apply(proto, data);
 
        if (onComplete) {
            onComplete.call(ctor, ctor);
        }
 
        if (singleton) {
            return new ctor();
        }
 
        return ctor;
    };
 
    ST.emptyFn = function () {};
 
    /**
     * Iterates an array or an iterable value and invoke the given callback function for
     * each item.
     *
     *     var countries = ['Vietnam', 'Singapore', 'United States', 'Russia'];
     *
     *     ST.each(countries, function(name, index, countriesItSelf) {
     *         console.log(name);
     *     });
     *
     *     var sum = function() {
     *         var sum = 0;
     *
     *         ST.each(arguments, function(value) {
     *             sum += value;
     *         });
     *
     *         return sum;
     *     };
     *
     *     sum(1, 2, 3); // returns 6
     *
     * The iteration can be stopped by returning `false` from the callback function.
     * Returning `undefined` (i.e `return;`) will only exit the callback function and
     * proceed with the next iteration of the loop.
     *
     *     ST.each(countries, function(name, index, countriesItSelf) {
     *         if (name === 'Singapore') {
     *             return false; // break here
     *         }
     *     });
     *
     * @param {Array|NodeList} iterable The value to be iterated.
     * TODO this fn param doc renders strange.... Function< /a>
     * @param {Function} fn The callback function. If it returns `false`, the iteration
     * stops and this method returns the current `index`. Returning `undefined` (i.e
     * `return;`) will only exit the callback function and proceed with the next iteration
     * in the loop.
     * @param {Object} fn.item The item at the current `index` in the passed `array`
     * @param {Number} fn.index The current `index` within the `array`
     * @param {Array} fn.allItems The `array` itself which was passed as the first argument
     * @param {Boolean} fn.return Return `false` to stop iteration.
     * @param {Object} [scope] The scope (`this` reference) in which the specified function
     * is executed.
     * @param {Boolean} [reverse=false] Reverse the iteration order (loop from the end to
     * the beginning).
     * @return {Boolean} If no iteration returns `false` then this method returns `true`.
     * Otherwise this method returns the index that returned `false`. See description for
     * the `fn` parameter.
     * @method each
     * @member ST
     */
    ST.each = function (iterable, fn, scope, reverse) {
        if (iterable) {
            var ln = iterable.length,
                i;
 
            if (reverse !== true) {
                for (= 0; i < ln; i++) {
                    if (fn.call(scope || iterable[i], iterable[i], i, iterable) === false) {
                        return i;
                    }
                }
            }
            else {
                for (= ln - 1; i > -1; i--) {
                    if (fn.call(scope || iterable[i], iterable[i], i, iterable) === false) {
                        return i;
                    }
                }
            }
 
            return true;
        }
    };
 
    ST.eachKey = function (obj, fn) {
        if (!obj) {
            return;
        }
        for (var key in obj) {
            fn(key, obj[key]);
        }
    };
 
    /**
     * Returns the first matching key corresponding to the given value.
     * If no matching value is found, null is returned.
     * @param {Object} object 
     * @param {Object} value The value to find
     * @method getKey
     * @member ST
     * @private
     */
    ST.getKey = function (object, value) {
        for (var property in object) {
            if (object.hasOwnProperty(property) && object[property] === value) {
                return property;
            }
        }
 
        return null;
    };
 
    /**
     * Gets all values of the given object as an array.
     * @param {Object} object 
     * @return {Array} An array of values from the object
     * @method getValues
     * @member ST
     * @private
     */
    ST.getValues = function (object) {
        var values = [],
            property;
 
        for (property in object) {
            if (object.hasOwnProperty(property)) {
                values.push(object[property]);
            }
        }
 
        return values;
    };
 
    ST.isArray = ('isArray' in Array) ? Array.isArray : function(value) {
        return toString.call(value) === '[object Array]';
    };
 
    ST.isBoolean = function (value) {
        return typeof value === 'boolean';
    };
 
    ST.isEmpty = function (value) {
        return (value == null) || (value && ST.isArray(value) && !value.length);
    };
 
    ST.isNumber = function (value) {
        return typeof value === 'number';
    };
 
    ST.isPrimitive = function (value) {
        var t = typeof value;
 
        return t === 'string' || t === 'number' || t === 'boolean';
    };
 
    ST.isString = function (value) {
        return typeof value === 'string';
    };
 
    //---------------------------------------------------------------------- 
    // Array 
 
    var slice = Array.prototype.slice,
        fixArrayIndex = function (array, index) {
            return (index < 0) ? Math.max(0, array.length + index)
                : Math.min(array.length, index);
        },
        replaceSim = function (array, index, removeCount, insert) {
            var add = insert ? insert.length : 0,
                length = array.length,
                pos = fixArrayIndex(array, index);
 
            // we try to use Array.push when we can for efficiency... 
            if (pos === length) {
                if (add) {
                    array.push.apply(array, insert);
                }
            } else {
                var remove = Math.min(removeCount, length - pos),
                    tailOldPos = pos + remove,
                    tailNewPos = tailOldPos + add - remove,
                    tailCount = length - tailOldPos,
                    lengthAfterRemove = length - remove,
                    i;
 
                if (tailNewPos < tailOldPos) { // case A 
                    for (= 0; i < tailCount; ++i) {
                        array[tailNewPos+i] = array[tailOldPos+i];
                    }
                } else if (tailNewPos > tailOldPos) { // case B 
                    for (= tailCount; i--; ) {
                        array[tailNewPos+i] = array[tailOldPos+i];
                    }
                } // else, add == remove (nothing to do) 
 
                if (add && pos === lengthAfterRemove) {
                    array.length = lengthAfterRemove; // truncate array 
                    array.push.apply(array, insert);
                } else {
                    array.length = lengthAfterRemove + add; // reserves space 
                    for (= 0; i < add; ++i) {
                        array[pos+i] = insert[i];
                    }
                }
            }
 
            return array;
        },
        replaceNative = function (array, index, removeCount, insert) {
            if (insert && insert.length) {
                // Inserting at index zero with no removing: use unshift 
                if (index === 0 && !removeCount) {
                    array.unshift.apply(array, insert);
                }
                // Inserting/replacing in middle of array 
                else if (index < array.length) {
                    array.splice.apply(array, [index, removeCount].concat(insert));
                }
                // Appending to array 
                else {
                    array.push.apply(array, insert);
                }
            } else {
                array.splice(index, removeCount);
            }
            return array;
        },
 
        eraseSim = function (array, index, removeCount) {
            return replaceSim(array, index, removeCount);
        },
 
        eraseNative = function (array, index, removeCount) {
            array.splice(index, removeCount);
            return array;
        },
 
        spliceSim = function (array, index, removeCount) {
            var pos = fixArrayIndex(array, index),
                removed = array.slice(index, fixArrayIndex(array, pos+removeCount));
 
            if (arguments.length < 4) {
                replaceSim(array, pos, removeCount);
            } else {
                replaceSim(array, pos, removeCount, slice.call(arguments, 3));
            }
 
            return removed;
        },
 
        spliceNative = function (array) {
            return array.splice.apply(array, slice.call(arguments, 1));
        },
 
        supportsSplice = (function () {
            var array = [],
                lengthBefore,
                j = 20;
 
            if (!array.splice) {
                return false;
            }
 
            // This detects a bug in IE8 splice method: 
            // see http://social.msdn.microsoft.com/Forums/en-US/iewebdevelopment/thread/6e946d03-e09f-4b22-a4dd-cd5e276bf05a/ 
 
            while (j--) {
                array.push("A");
            }
 
            array.splice(15, 0, "F", "F", "F", "F", "F","F","F","F","F","F","F","F","F","F","F","F","F","F","F","F","F");
 
            lengthBefore = array.length; //41 
            array.splice(13, 0, "XXX"); // add one element 
 
            if (lengthBefore + 1 !== array.length) {
                return false;
            }
            // end IE8 bug 
 
            return true;
        }()),
 
        erase = supportsSplice ? eraseNative : eraseSim,
        replace = supportsSplice ? replaceNative : replaceSim,
        splice = supportsSplice ? spliceNative : spliceSim;
 
    ST.Array = STArray = {
        erase: erase,
        replace: replace,
        // Note: IE8 will return [] on slice.call(x, undefined). 
        slice: ([1,2].slice(1, undefined).length ?
            function (array, begin, end) {
                return slice.call(array, begin, end);
            } :
            function (array, begin, end) {
                // see http://jsperf.com/slice-fix 
                if (typeof begin === 'undefined') {
                    return slice.call(array);
                }
                if (typeof end === 'undefined') {
                    return slice.call(array, begin);
                }
                return slice.call(array, begin, end);
            }
        ),
        splice: splice,
        insert: function (array, index, items) {
            return replace(array, index, 0, items);
        },
        indexOf: function (array, item) {
            if (array.indexOf) {
                return array.indexOf(item);
            }
 
            for (var i = 0, n = array.length; i < n; i++) {
                if (array[i] === item) {
                    return i;
                }
            }
 
            return -1;
        },
        remove: function (array, item) {
            var index = STArray.indexOf(array, item);
            if (index >= 0) {
                erase(array, index, 1);
            }
        },
        toMap: function (array) {
            var ret = {},
                i;
 
            for (= array && array.length; i-- > 0; ) {
                ret[array[i]] = 1;
            }
 
            return ret;
        }
    };
 
    ST.String = STString = {
        spaceRe: /[ ]+/g,
        trimRe: /^\s+|\s+$/g,
 
        startsWith: function (s, prefix) {
            return s.length >= prefix.length && s.indexOf(prefix) === 0;
        },
 
        split: function (s) {
            return s ? s.split(STString.spaceRe) : [];
        },
 
        trim: function (s) {
            return s ? s.replace(STString.trimRe, '') : '';
        }
    }
 
    //---------------------------------------------------------------------- 
 
    ST.Observable = ST.define({
        _update: function (add, name, fn, scope, opts) {
            var me = this,
                array, entry, i, key, keys, listeners, n, old;
 
            if (typeof name !== 'string') {
                for (key in name) {
                    if (key !== 'scope' && key !== 'single') {
                        if (typeof(fn = name[key]) === 'function') {
                            opts = name;
                        } else {
                            opts = fn;
                            fn = opts.fn;
                        }
 
                        me._update(add, key, fn, name.scope, opts);
                    }
                }
            }
            else {
                listeners = me._listeners || (me._listeners = {});
                array = listeners[name] || (listeners[name] = []);
 
                opts = ST.apply({
                    scope: scope,
                    fn: fn
                }, opts);
 
                if (add) {
                    if (array.firing) {
                        listeners[name] = array = array.slice();
                    }
 
                    array.push(opts);
                } else {
                    // Array.splice() is bugged in IE8, so avoid it (which is 
                    // easy since we often need to make a new array anyway): 
                    old = array;
                    array = null;
 
                    for (= 0, n = old.length; i < n; ++i) {
                        entry = old[i];
 
                        if (array) {
                            array.push(entry);
                        }
                        else if (opts.fn === entry.fn && opts.scope === entry.scope &&
                            opts.single === entry.single) {
                            listeners[name] = array = old.slice(0, i);
                        }
                    }
                }
            }
        },
 
        on: function (name, fn, scope, opts) {
            this._update(true, name, fn, scope, opts);
        },
 
        un: function (name, fn, scope, opts) {
            this._update(false, name, fn, scope, opts);
        },
 
        fireEvent: function (name) {
            var me = this,
                listeners = me._listeners,
                array = listeners && listeners[name],
                args, entry, fn, i, len, ret, scope;
 
            if (!(len = array && array.length)) {
                return;
            }
 
            args = Array.prototype.slice.call(arguments, 1);
            array.firing = (array.firing || 0) + 1;
 
            for (= 0; i < len; i++) {
                entry = array[i];
                ret = (fn = entry.fn).apply((scope = entry.scope) || me, args);
 
                if (entry.single) {
                    me.un(name, fn, scope, entry);
                }
 
                if (ret === false) {
                    break;
                }
            }
 
            array.firing--;
 
            return ret;
        }
    });
})(ST);