/**
 * @class ST.context.WebDriver
 */
(function() {
    var DEBUG_ME = false,
        WebDriver;
 
    ST.context.WebDriver = ST.define({
        extend: ST.context.Base,
 
        /**
         * @cfg {Object} driverConfig 
         * Object which should contain a desiredCapabilities property which conforms to the
         * webdriver desired capabilities {@link https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities}
         * specification. Such as:
         *
         *      var ctx = new ST.context.WebDriver({
         *          driverConfig: {
         *              desiredCapabilities: {
         *                  browserName: 'chrome'
         *              }
         *          }
         *      });
         */
 
        /**
         * @cfg {String} subjectUrl 
         * The URL to open when init() is called.
         */
 
        /**
         * @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,
 
        _STContent: "",
 
        isWebDriverContext: true,
 
        hbRE: /.*STHB:/,
        msgRE: /.*STM:/,
 
        /**
         * Construct a new WebDriver context and pre-fetch the ST context.
        */
        constructor: function (config) {
            var me = this;
 
            ST.context.WebDriver.superclass.constructor.call(me, config);
 
            ST.apply(me, config);
 
            me.targetMessages = [];
            me.targetLogs = [];
 
            // TODO put these in the magical Studio preferences file for manual tweaking in the field. 
            me.logPollInterval = 500;
            me.heartbeatInterval = 250;
            me.heartbeatFailureThreashold = 3000;
 
            me._prefetchSTContent();
        },
 
        /**
         * Opens the desired browser, initializes it, loads the subjectUrl in the browser,
         * and loads Sencha Test framework into target browser so that the context is ready
         * for use with Futures API methods.
         */
        init: function () {
            var me = this;
 
            return new Promise(function (resolve, reject) {
                if (DEBUG_ME) debugger
 
                me.driver = ST.webdriverio
                    .remote(me.driverConfig)
                    .init()
                    .then(function (ret) {
                        if (ST.LOGME) console.log('webdriver init ret='+JSON.stringify(ret));
                        if (ret.value && ret.value.browserName && ret.value.browserName === 'chrome') {
                            me.LOGS_SUPPORTED = true;
                            me.pollLogs();
                        } else {
                            me.LOGS_SUPPORTED = false;
                        }
                    }, function (err) {
                        var message;
                        if (err.seleniumStack) {
                            message = err.seleniumStack;
                        } else {
                            message = err.message || err;
                        }
                        reject(message);
                    })
                    .timeouts('script', 10 * 1000)
                    .then(function (ret) {
                        me.driver.addCommand('STmoveToObject',me._moveToObject)
 
                        if (me.subjectUrl) {
                            me.driver.url(me.subjectUrl).then(function () {
                                me
                                    .initBrowserInfo()
                                    .then(function () {
                                        me._loadST().then(resolve, reject);
                                    });
                            }, function (err) {
                                    reject('Error loading url: ' + me.subjectUrl + ', err: ' + err);
                                });
                        } else {
                            if (ST.LOGME) console.log('no subjectUrl in WebDriver constructor config options...');
                            resolve();
                        }
                    })
                    .catch(function (err) {
                        if (ST.LOGME) console.log('webdriver init err=',err);
                        reject(err);
                    });
            });
        },
 
        initEvent: function (event) {
            if (!ST.playable.Playable.isRemoteable(event)) {
                return this.createPlayable(event);
            } else {
                // for WebDriver we want the player to keep the "bare" Playable config 
                // since we create the playable in the target only. 
                return event;
            }
        },
 
        initBrowserInfo: function () {
            var me = this,
                driver = me.driver,
                getBrowserInfo = me.remoteHelpers.getBrowserInfo;
 
            return driver.execute(getBrowserInfo)
                .then(function (result) {
                    var browserInfo = result.value;
                    ST._initBrowser(browserInfo.navigator.userAgent);
                    ST._initOS({
                        navigator: browserInfo.navigator,
                        window: browserInfo.window
                    })
                });
        },
 
        _prefetchSTContent: function () {
            var fs = require('fs'),
                path = require('path'),
                serveDir = ST.serveDir,
                files = [
                    'init.js',
                    'supports.js',
                    'base.js',
                    'Version.js',
                    'common.js',
                    'Timer.js',
                    'setup-ext.js',
                    'context/Base.js',
                    'context/Local.js',
                    'context/LocalWebDriver.js',
                    'Browser.js',
                    'OS.js',
                    'Element.js',
                    'KeyMap.js',
                    'Alert.js',
                    'event/Event.js',
                    'event/GestureQueue.js',
                    'event/wgxpath.install.js',
                    'Locator.js',
                    'locator/Strategy.js',
                    'event/Driver.js',
                    'event/Recorder.js',
                    'playable/Playable.js',
                    'playable/State.js',
                    'future/Element.js',
                    'future/Component.js',
                    'tail.js'
                ],
                content = {};
 
            for (var i=0; i<files.length; i++) {
                content[files[i]] = fs.readFileSync(path.join(serveDir, files[i]), {encoding:'utf8'});
            }
 
            this._STContent = content;
        },
 
        getLogs: function () {
            var me = this,
                driver = me.driver,
                logItems, logMessage, message;
 
            if (!me.logPromise && me.LOGS_SUPPORTED) {
                me.logPromise = driver.log('browser').then(function (logs) {
                    me.logPromise = null;
                    logItems = logs.value;
                    logItems.forEach(function (log) {
                        logMessage = log.message;
                        if (me.msgRE.test(logMessage)) {
 
                            /** Log Detection
                             *  We have to do some logging-type detection
                             * Chrome logs thusly:
                             * console-api 141:16 "STM:{\"to\":\"runner\",\"type\":\"recordingStarted\",\"seq\":1}"
                             * Embedded logs thusly:
                             * console-api 142:17 STM:{"to":"runner","type":"recordingStarted","seq":1}
                             */
                            if (logMessage.indexOf('"STM:') !== -1) {
                                // Start with Chrome detection 
                                // Found Chrome log type 
                                logMessage = JSON.parse(logMessage.substr(logMessage.indexOf('"STM:')));
                                message = JSON.parse(logMessage.substr(logMessage.indexOf('STM:') + 4));
                            } else {
                                message = JSON.parse(logMessage.substr(logMessage.indexOf('STM:') + 4));
                            }
                            if (message.to === 'runner') {
                                ST.sendMessage(message);
                            } else if (message.to !== 'sandbox') {
                                debugger;
                            } else {
                                if (message.type === 'navigate') {
                                    if (me.isRecording) {
                                        me._loadST().then(function () {
                                            me.startRecording();
                                        }, function (err) {
                                            // Problem loading ST 
                                            if (ST.LOGME) console.log('==========> problem after naviagtion');
                                            me.handleClientError(err);
                                        }).catch(function (err) {
                                            // Problem starting recorder 
                                            me.handleClientError(err);
                                        });
                                    }
                                }
                            }
                        } else if (me.hbRE.test(log)) {
                            if (ST.LOGME) console.log('WebDriver.getLogs: got heartbeat message');
                            // TODO update lastheartbeattime thingamabob 
                        } else {
                            me.targetLogs.push(log);
                        }
                    });
                    me.logTargetConsole();
                    // TODO determine failure threshold(s), execute either an ST reload or utter failure back to Studio 
                    // Need to use the heartbeating bits to determine connectivity to target browser before full failure 
                }, function (err) {
                    if (ST.LOGME) console.log('WebDriver.getLogs: Log retrieval failed.', err);
                    me.handleClientError(err);
                }).catch(function (err) {
                    me.handleClientError(err);
                });
            }
 
            return me.logPromise;
 
            // TODO: error handling of log polling... 
            // error when browser quits in selenium log: 
            // Unable to evaluate script: disconnected: not connected to DevTools 
            // but that error isn't returned in the driver log response... boo 
        },
 
        handleClientError: function (error) {
            // init handles webdriver launch errors, so we want to know when something happened. 
            // maybe we don't care if the target went away and should just stop whatever Studio was doing. 
            if (error.type === 'NoSessionIdError') {
                // Target browser went away 
                // TODO send message to Studio? 
            } else if (error.type === 'RuntimeError') {
                if (error.seleniumStack) {
                    if (error.seleniumStack.status === '6') {
                        // Target browser went away 
                        // TODO send message to Studio? 
                    }
                }
            } else {
                debugger;
            }
        },
 
        logTargetConsole: function () {
            var me = this,
                logItems = me.targetLogs;
 
            if (!me.logPromise) {
                me.targetLogs = [];
 
                if (logItems.length) {
                    logItems.forEach(function (log) {
                        if (ST.LOGME) {
                            console.log(ST.LOGME+''+log.message);
                            ST.sendMessage({
                                type: 'targetLog',
                                message: ST.LOGME+''+log.message
                            });
                        }
                    });
                }
            }
        },
 
        pollLogs: function () {
            var me = this;
 
            if (!me.logTimer && me.LOGS_SUPPORTED) {
                me.logTimer = setInterval(function () {
                    me.getLogs();
                }, me.logPollInterval);
            }
        },
 
        startTargetHeartbeat: function () {
            var me = this,
                driver = me.driver;
 
            driver.execute(function (_heartbeatInterval) {
                // IN-BROWSER-BEGIN 
                ST.defaultContext.startHeartbeat(_heartbeatInterval);
                // IN-BROWSER-END 
            }, me.heartbeatInterval).catch(function (err) {
                // TODO couldn't start the heartbeat, probably should fallback to Studio 
                if (ST.LOGME) console.log('Something happened on the way to heartbeat.', err);
                me.handleClientError(err);
            });
        },
 
        isRecordingPlayable: function (playable) {
            return typeof playable.recorderId !== 'undefined';
        },
 
        remoteHelpers: {
            getBrowserInfo: function () {
                return {
                    navigator: {
                        userAgent: navigator.userAgent,
                        platform: navigator.platform
                    },
                    window: {
                        deviceType: window.deviceType,
                        location: {
                            search: window.location.search || ''
                        }
                    }
                }
            },
 
            getSelectorForTarget: function (target) {
                var el = typeof target === 'object' ? target : ST.Locator.find(target),
                    id = new Date().getTime(),
                    attr = 'data-senchatestid';
 
                // make sure we found something!!! 
                if (el) {
                    // if we don't already have an attribute, add it with the value 
                    if (!el.hasAttribute(attr)) {
                        el.setAttribute(attr, id);
                    } 
                    // otherwise, we'll just use the one that was previously assigned 
                    else {
                        id = el.getAttribute(attr);
                    }
 
                    // compose the selector, job done!! 
                    return "[" + attr + "='" + id + "']";
                } else {
                    // no happy results...return root 
                    return '@';
                }         
            },
 
            isScrollable: function (x, y, el) {
                var el = el || document.body,
                    dimEl = el === document.body ? document.documentElement : el,
                    scrollSize = ST.getScrollbarSize(),
                    isYScrollable = el.scrollHeight > dimEl.clientHeight,
                    isXScrollable = el.scrolLWidth > dimEl.clientWidth,
                    isScrollable = isYScrollable || isXScrollable,
                    box = el.getBoundingClientRect(),
                    rightGap = box.right - scrollSize.width,
                    bottomGap = box.bottom - scrollSize.height;
 
                return isScrollable && (>= (rightGap) || (>= bottomGap));
 
            },
            
            handleWheel: function (_coords, _deltas, _element) {
                var me = this,
                    el = _element || document.elementFromPoint(_coords.x, _coords.y),
                    oldScroll = {};
 
                // this method inspects the element and determines if the scroll position is such that it can  
                // respond to the passed inputs 
                // we want to be able keep going up the node tree if the target for scrolling is already at its 
                // min/max limits, so that the scroll can continue to "overflow" up the hierarchy, like it does 
                // with an actual browser scroll 
                var canScroll = function (node, x, y) {
                    var canScrollX = false,
                        canScrollY = false,
                        yScroll = node.scrollHeight > node.clientHeight,
                        xScroll = node.scrollWidth > node.clientWidth,
                        atTop = node.scrollTop === 0,
                        atBottom = (node.scrollHeight - node.clientHeight) === node.scrollTop,
                        atLeft = node.scrollLeft === 0,
                        atRight = (node.scrollWidth - node.clientWidth) === node.scrollLeft;
 
                    if (> 0) {
                        canScrollX = xScroll && !atRight;
                    } else if (< 0) {
                        canScrollX = xScroll && !atLeft;
                    }
 
                    if (> 0) {
                        canScrollY = yScroll && !atBottom;
                    } else if (< 0) {
                        canScrollY = yScroll && !atTop;
                    }
 
                    return canScrollX || canScrollY;
                };
 
                var isScrollable = function (node) {
                    if (node === null) {
                        return null;
                    }
 
                    if (node === document.body || canScroll(node, _deltas.x, _deltas.y)) {
                        return node;
                    } else {
                        return isScrollable(node.parentNode);
                    }
                };
 
                var doScroll = function (node) {
                    var scrollable = isScrollable(node);
 
                    // the scrollable will either be the passed-in target, or the next scrollable ancestor 
                    if (scrollable) {
                        oldScroll.top = scrollable.scrollTop;
                        oldScroll.left = scrollable.scrollLeft;
 
                        scrollable.scrollTop = scrollable.scrollTop + (_deltas.y);
                        scrollable.scrollLeft = scrollable.scrollLeft + (_deltas.x);
 
                        // if no scrolling actually occurs, we need to recurse since scrollHeights might be askew for some reason 
                        if (scrollable !== document.body && oldScroll.top === scrollable.scrollTop && oldScroll.left === scrollable.scrollLeft) {
                            doScroll(scrollable.parentNode);
                        }
                    }
                }
                
                doScroll(el);                
            },
 
            getScrollPosition: function (_pos, _element) {
                var el = _element;
 
                var isScrollable = function (node) {
                    if (node === null) {
                        return null;
                    }
 
                    if (node.scrollHeight > node.clientHeight || node.scrollWidth > node.clientWidth) {
                        return node;
                    } else {
                        return isScrollable(node.parentNode);
                    }
                };
                var scrollable = isScrollable(el);
                // the scrollable will either be the passed-in target, or the next scrollable ancestor 
                if (scrollable) {
                    scrollable.scrollTop = _pos[1];
                    scrollable.scrollLeft = _pos[0];
                }
            },
 
            getElementOffsets: function (x, y, target) {
                var failOnMultiple = ST.options.failOnMultipleMatches,
                    offsetX = x,
                    offsetY = y,
                    rect, el;
 
                // force no failures on multiple matches 
                ST.options.failOnMultipleMatches = false;
                el = ST.Locator.find(target);
                // restore settings 
                ST.options.failOnMultipleMatches = failOnMultiple;
 
                if (el) {
                    rect = el.getBoundingClientRect();
 
                    offsetX = Math.floor(- rect.left);
                    offsetY = Math.floor(- rect.top);
                }
 
                return {
                    offsetX: offsetX,
                    offsetY: offsetY
                };
            }
        },
 
        inject: function (playable, resolve, reject) {
            var me = this,
                type = playable.type,
                isRP = me.isRecordingPlayable(playable),
                webElement;
 
            // for tap/focus/others... target could be me.locator and take care of this? 
            if (playable.webElement || playable.future) {
                webElement = playable.webElement || playable.future.webElement; // TODO how to resolve this? 
            } else {
                webElement = {};
            }
 
            if (playable.args) {
                playable.x = playable.x || playable.args.x
                playable.y = playable.y || playable.args.y
            }
            
            if (!playable.args) playable.args = {};
 
            if (DEBUG_ME) debugger;
 
            if (!ST.playable.Playable.isRemoteable(playable)) {
                // if an event is not remoteable and it isn't one of the below then 
                // it is likely a wait(<milliseconds>) call or similar. 
                // debugger 
            }
 
            // NOTE: playable.targetEl is a WebJSON Element, so use .ELEMENT to provide the ID to JSONWire protocol 
            // methods below... 
            if (type === 'mousedown') {
                if (isRP) {
                    me.driver
                        .execute(me.remoteHelpers.getElementOffsets, playable.pageX, playable.pageY, playable.target)
                        .then(function (_offsets) {
                            var offsets = _offsets.value;
 
                            playable.x = offsets.offsetX;
                            playable.y = offsets.offsetY;
                            playable.xy = [offsets.offsetX, offsets.offsetY];
 
                            me.driver
                                .execute(me.remoteHelpers.isScrollable, playable.pageX, playable.pageY, playable.webElement)
                                .then(function (_isScrollbarClick) {
                                    playable.isScrollbarClick = _isScrollbarClick.value;
 
                                    // Oh travesty! Oh abomination! But if this is click on a scrollbar, we need to store the element for later 
                                    // so we can interrogate it for its scroll position. 
                                    if (playable.isScrollbarClick) {
                                        me.scrollClickElement = playable.webElement;
                                    } else {
                                        // if not a scroll click, for the love of all that is holy, delete it!!! 
                                        delete me.scrollClickElement;  
                                    }
 
                                    me.driver.buttonDown().then(resolve,reject);
                                });
                        });                        
                } else {
                    me.driver
                        .execute(me.remoteHelpers.getSelectorForTarget, playable.webElement)
                        .then(function (_selector) {
                            me.driver
                                // TODO: x/y need to be offsets to the element, currently they are page offsets :( 
                                .STmoveToObject(_selector.value, playable.x, playable.y)
                                .buttonDown()
                                .then(resolve, reject);
                    });
                }
                
            } else if (type === 'mouseup') {
                if (isRP) {
                    if (me.scrollClickElement) {
                        // if we're in the middle of a yet-to-be-resolved scrollClick, we need to retrieve the scroll position 
                        // from the initially clicked element so we can provide those positions when they are needed 
                        me.driver.execute(function (_element) {
                            return {
                                x: _element.scrollLeft,
                                y: _element.scrollTop
                            };
                        }, me.scrollClickElement)
                        .then(function (_scrollPos) {
                            // store the scroll position on the playable so we can retrieve it later when needed 
                            playable.finalScrollPosition = _scrollPos.value;
                            me.driver.buttonUp().then(resolve,reject);
                        });
                    } else {
                        me.driver
                            .execute(me.remoteHelpers.getElementOffsets, playable.pageX, playable.pageY, playable.target)
                            .then(function (_offsets) {
                                var offsets = _offsets.value;
 
                                playable.x = offsets.offsetX;
                                playable.y = offsets.offsetY;
                                playable.xy = [offsets.offsetX, offsets.offsetY];
 
                                me.driver.buttonUp().then(resolve,reject);
                        });
                    }
                } else {
                    // no scroll clicking in progress, just run the buttonUp 
                    me.driver
                        .execute(me.remoteHelpers.getSelectorForTarget, playable.webElement)
                        .then(function (_selector) {
                            me.driver
                                // TODO: x/y need to be offsets to the element, currently they are page offsets :( 
                                .STmoveToObject(_selector.value, playable.x, playable.y)
                                .buttonUp()
                                .then(resolve, reject);
                    });
                }                
            } else if (type === 'scroll' ) {
                me.driver.execute(me.remoteHelpers.getScrollPosition, playable.pos, webElement)
                .then(resolve,reject);
            } else if (type === 'wheel' ) {
                // if we have a web element, we'll want to use it; otherwise, let's null it out so it doesn't cause problems 
                if (typeof webElement !== 'object' || !webElement.ELEMENT) {
                    webElement = null;
                }
                if (playable.deltaX !== 0 || playable.deltaY !== 0) {
                    var coords = {x: playable.pageX, y:playable.pageY},
                        deltas = {x: playable.deltaX, y: playable.deltaY};
 
                    me.driver.execute(me.remoteHelpers.handleWheel, coords, deltas, webElement).then(resolve,reject);
                }
            } else if (type === 'tap' || type === 'click') {
                if (isRP) {
                    if (!me.scrollClickElement) {
                        resolve();
                    }
                } else {
                    me.driver
                        .execute(me.remoteHelpers.getSelectorForTarget, playable.webElement)
                        .then(function (_selector) {
                            me.driver
                                .STmoveToObject(_selector.value, playable.x, playable.y)
                                .buttonDown()
                                .buttonUp()
                                .then(resolve, reject);
                        });
                }   
            } else if (type === 'dblclick') {
                me.driver
                    .execute(me.remoteHelpers.getSelectorForTarget, playable.webElement)
                    .then(function (_selector) {
                        me.driver
                            .doubleClick(_selector.value)
                            .then(resolve, reject);
                });
            } else if (type === 'rightclick') {
                // right click is a gesture, so will only ever playback... 
                me.driver
                    .execute(me.remoteHelpers.getSelectorForTarget, playable.webElement)
                    .then(function (_selector) {
                        me.driver
                            // TODO: x/y need to be offsets to the element, currently they are page offsets :( 
                            .rightClick(_selector.value, playable.x || 0, playable.y || 0)
                            .then(resolve, reject);
                }); 
            } else if (type === 'type') {
                // TODO support args.caret 
                var text = playable.args.text || playable.text,
                    key = playable.args.key || playable.key,
                    caret = playable.args.caret || playable.caret;
 
                text = text || key;
                if (key === 'Backspace') {
                    text = String.fromCharCode(8);
                } else if (key === 'ArrowLeft') {
                    text = 'Left arrow';
                } else if (key === 'ArrowRight') {
                    text = 'Right arrow';
                } else if (key === 'ArrowUp') {
                    text = 'Up arrow';
                } else if (key === 'ArrowDown') {
                    text = 'Down arrow';
                }  else if (key === 'CapsLock') {
                    resolve();
                    return;
                }
 
                if (webElement) {
                    me.driver.elementIdValue(webElement.ELEMENT, text).then(resolve, reject);
                } else {
                    me.driver.keys(text).then(resolve, reject);
                }
            } else if (playable.type === 'keydown') {
                var key = playable.args.key || playable.key;
 
                if (key === 'Backspace') {    // TODO FnKeys 
                    me.driver.keys(String.fromCharCode(8)).then(resolve, reject);
                } else if (key === 'ArrowLeft') {
                    me.driver.keys('Left arrow').then(resolve, reject);
                } else if (key === 'ArrowRight') {
                    me.driver.keys('Right arrow').then(resolve, reject);
                } else if (key === 'ArrowUp') {
                    me.driver.keys('Up arrow').then(resolve, reject);
                } else if (key === 'ArrowDown') {
                    me.driver.keys('Down arrow').then(resolve, reject);
                }  else if (key === 'CapsLock') {
                    resolve();
                } else {
                    me.driver.keys(key).then(resolve, reject);
                }
            } else if (playable.type === 'keyup') {
                // webdriverio leaves modifier keys 'pressed' until pressed again 
                // http://webdriver.io/api/protocol/keys.html 
                if (playable.key === 'Shift'    ||
                    playable.key === 'Control'  ||
                    playable.key === 'Alt'      ||
                    playable.key === 'Meta') {  // TODO add other modifier keys (Fn, PgUp/Dn, etc.)? 
                    // release all the keys 
                    me.driver.keys('NULL').then(resolve, reject);
                } else {
                    resolve();
                }
            } else if (playable.type && playable.type !== 'wait') {
                resolve();
            } else {
                resolve();
            }
        },
 
        _remoteCallFn: function (playable, serialized, retry) {
            if (ST.LOGME) console.log('_remoteCallFn() serialized=',serialized);
            var me = this,
                driver = me.driver,
                ser = serialized;
 
            return new Promise(function (resolve, reject) {
                var start = new Date().getTime();
 
                if (DEBUG_ME) {
                    driver.timeouts('script', 30 * 60 * 1000);
                }
 
                driver.executeAsync(function (event, debug, remoteDone) {
                    // IN-BROWSER-BEGIN 
                    if (typeof ST === 'undefined') {
                        remoteDone({
                            loadST: true
                        });
                        throw('ST_NEED_ST_EXCEPTION');
                    }
 
                    if (ST.LOGME) console.log('call, event=',event);
 
                    ST.DEBUG = debug;
 
                    window.$ST = window.$ST || {};
                    window.$ST.call = function () {
                        if (ST.DEBUG) debugger
 
                        var playable = ST.defaultContext.createPlayable(event),
                            future = playable.future,
                            promise, start;
 
                        start = new Date().getTime();
 
                        if (typeof playable.fn !== 'function') {
                            remoteDone({
                                error: 'playable.fn is not a function'
                            });
                            return;
                        }
 
                        var localDone = function (result) {
                            var retValue = {
                                playable: {
                                    webElement: playable.targetEl && playable.targetEl.dom
                                },
                                future: {
                                    webElement: future.el && future.el.dom
                                },
                                data: playable.future.data,
                                locatorWebElement: future.locator && future.locator.targetEl && future.locator.targetEl.dom,
                                duration: new Date().getTime() - start
                            };
                            retValue.error = result && result.error;
 
                            remoteDone(retValue);
                        };
                        localDone.fail = function (err) {
                            localDone({
                                error: err.toString()
                            });
                        };
 
                        promise = playable.fn.call(playable, localDone);
 
                        if (promise && typeof promise.then === 'function') {
                            promise.then(function () {
                                localDone();
                            }, function (err) {
                                localDone({
                                    error: err.toString()
                                });
                            });
                        } else if (!playable.fn.length) {
                            localDone();
                        }
                    };
 
                    window.$ST.call();
                    // IN-BROWSER-END 
                }, ser, DEBUG_ME).then(function (ret) {
                    if (ST.LOGME) console.log('WebDriver._remoteCallFn, executeAsync resolved with '+JSON.stringify(ret));
                    if (DEBUG_ME) debugger
 
                    if (ST.options.webdriverPerf) {
                        ST.status.addResult({
                            passed: true,
                            message: 'WebDriver._remoteCallFn.browser.duration.'+playable.type+'='+ret.value.duration
                        });
                        ST.status.addResult({
                            passed: true,
                            message: 'WebDriver._remoteCallFn.sandbox.duration.'+playable.type+'='+(new Date().getTime()-start)
                        });
                    }
 
                    var v = ret.value,
                        loadST = v.loadST,
                        error = v.error;
 
                    if (error) {
                        reject(error);
                        return;
                    }
 
                    if (loadST && retry) {
                        me._loadST().then(function () {
                            me._remoteCallFn(playable, ser, false).then(resolve, reject);
                        }, reject);
                    } else {
                        {
                            ST.apply(playable, v.playable); // apply targetDom because it might have been reconstructed 
                            ST.apply(playable.future, v.future);
                            if (v.data && playable.future) {
                                playable.future.setData(v.data);
                            }
                            if (v.locatorWebElement) {
                                playable.future.locator.webElement = v.locatorWebElement;
                            }
                            
 
                            resolve(ret.value);
                        }
                    }
                }, function (err) {
                    if (ST.LOGME) console.log('callFn remote, err=',err);
                    if (DEBUG_ME) debugger
 
                    if (err.seleniumStack && err.seleniumStack.type === 'StaleElementReference') {
                        playable.webElement = undefined;
                        playable.future.webElement = undefined;
                        reject();
                        return;
                    }
 
                    reject(err);
                }).catch(function (err) {
                    if (ST.LOGME) console.log('callFn remote catch err=', err);
                });
            });
        },
 
        callFn: function (playable, done) {
            var me = this,
                serialized = me._serialize(playable);
 
            if (DEBUG_ME) debugger
 
            if (ST.playable.Playable.isRemoteable(playable)) {
                return me._remoteCallFn(playable, serialized, true); // one retry 
            } else {
                return playable.fn.call(playable, done);
            }
        },
 
        _serialize: function (playable) {
            var ret = {};
 
            for (var i in playable) {
                var t = typeof playable[i];
                var v = playable[i];
 
                if (!== 'object' && t !== 'function') {
                    ret[i] = v;
                }
                if (=== 'object' && v === null) {
                    ret[i] = v;
                }
            }
 
            ret.webElement = playable.webElement;
 
            if (playable.target) {
                if (typeof playable.target === 'string') {
                    ret.target = playable.target;
                } else {
                    ret.target = {};
                    ret.target.webElement = playable.target.webElement;
                    ret.target.locatorChain = playable.target.locatorChain;
                }
            }
 
            // take relatedCmps, strip out their webElement and send that instead... 
            ret.future = ret.future || {};
            ret.futureData = ST.apply({}, playable.instanceData);
            if (playable.future) {
                var retf = ret.future,
                    future = playable.future,
                    related = future.related;
 
                retf.related = {};
 
                for(var name in related) {
                    var relatedFuture = related[name];
                    retf.related[name] = {
                        webElement: relatedFuture.webElement,
                        futureClsName: relatedFuture.$className,
                        data: relatedFuture.data
                    };
                }
 
                if (future.locator) {
                    retf.locator = {
                        webElement: future.locator.webElement,
                        locatorChain: future.locator.locatorChain
                    };
                }
                
                retf.locatorChain = ret.root = future.locatorChain;
                retf.webElement = future.webElement;
                ret.futureClsName = future.$className;
                retf.data = future.data;
                retf.futureData = future.futureData;
            }
 
            ret.args = playable.args;
            ret.instanceData = playable.instanceData;
 
            return ret;
        },
 
        _remoteReadyFn: function (playable, serialized, retry) {
            if (ST.LOGME) console.log('_remoteReadyFn() serialized=',serialized);
            var me = this,
                driver = me.driver,
                ser = serialized;
 
            return new Promise(function (resolve,reject) {
                var start = new Date().getTime();
 
                if (DEBUG_ME) {
                    driver.timeouts('script', 30 * 60 * 1000);
                }
 
                driver.executeAsync(function (event, debug, done) {
                    var start = new Date().getTime();
 
                    // IN-BROWSER-BEGIN 
                    if (typeof ST === 'undefined') {
                        done({ loadST: true });
                    }
 
                    ST.DEBUG = debug;
 
                    window.$ST = window.$ST || {};
                    window.$ST.ready = function () {
                        if (ST.DEBUG) debugger
 
                        var playable = ST.defaultContext.createPlayable(event),
                            future = playable.future,
                            ready = false;
 
                        try {
                            ready = playable.ready();
                        } catch (e) {
                            if (ST.DEBUG) debugger
                            done({
                                error: e.message,
                                duration: new Date().getTime() - start
                            });
                            return;
                        }
 
                        done({
                            playable: {
                                webElement: playable.targetEl && playable.targetEl.dom,
                                waitingFor: playable.waitingFor,
                                waitingState: playable.waitingState
                            },
                            future: {
                                webElement: future.el && future.el.dom
                            },
                            data: future.data,
                            ready: ready,
                            duration: new Date().getTime() - start
                        });
                    };
 
                    window.$ST.ready();
                    // IN-BROWSER-END 
                }, ser, DEBUG_ME).then(function (ret) {
 
                    if (ST.LOGME) console.log('ready remote call playable.type='+playable.type+', ret=',ret);
 
                    if (ST.options.webdriverPerf) {
                        ST.status.addResult({
                            passed: true,
                            message: 'WebDriver._remoteReadyFn.browser.duration.'+playable.type+'='+ret.value.duration
                        });
                        ST.status.addResult({
                            passed: true,
                            message: 'WebDriver._remoteReadyFn.sandbox.duration.'+playable.type+'='+(new Date().getTime()-start)
                        });
                    }
 
                    if (DEBUG_ME) debugger
 
                    var v = ret.value,
                        ready = v.ready,
                        error = v.error,
                        loadST = v.loadST;
 
                    if (loadST && retry) {
                        me._loadST().then(function () {
                            me._remoteReadyFn(playable, ser, false).then(resolve, reject);
                        }, reject);
                    } else if (error) {
                        reject(error);
                    } else {
                        ST.apply(playable, v.playable);
                        ST.apply(playable.future, v.future);
                        if (v.data && playable.future) {
                            playable.future.setData(v.data);
                        }
 
                        ready ? resolve(ret) : reject();
                    }
                }, function (err) {
                    if (ST.LOGME) console.log('ready remote call, playable.type='+playable.type+', err=',err);
                    if (DEBUG_ME) debugger
 
                    if (err.seleniumStack && err.seleniumStack.type === 'StaleElementReference') {
                        playable.webElement = undefined;
                        playable.future.webElement = undefined;
                        reject();
                        return;
                    }
 
                    if (retry) {
                        me._loadST().then(function () {
                            me._remoteReadyFn(playable,ser,false).then(resolve,reject);
                        }, reject);
                    } else {
                        reject(err);
                    }
                }).catch(function (err) {
                    if (ST.LOGME) console.log('ready remote executeAsync catch, playable.type='+playable.type+', err='+err);
                });
            });
        },
 
        ready: function (playable, resolve, reject) {
            var me = this,
                serialized = me._serialize(playable);
            
            if (ST.playable.Playable.isRemoteable(playable)) {
                me._remoteReadyFn(playable, serialized, true /* one retry */).then(resolve, reject);
            } else {
                resolve();
            }
        },
 
        _checkGlobalLeaks: function (done, contextGlobals, retry) {
            if (ST.LOGME) console.log('WebDriver._checkGlobalLeaks()');
            var me = this,
                driver = me.driver,
                contextGlobals = contextGlobals || ST.options.contextGlobals;
 
            if (typeof retry === 'undefined') {
                retry = true;
            }
 
            driver.executeAsync(function (debug, contextGlobals, done) {
                if (typeof ST === 'undefined') {
                    done({ loadST: true });
                    return;
                }
 
                ST.DEBUG = debug;
 
                window.$ST = window.$ST || {};
                window.$ST.check = function () {
                    try {
                        var result;
 
                        if (ST.DEBUG) debugger
 
                        ST.addGlobals.apply(ST,contextGlobals);
 
                        result = ST.checkGlobalLeaks();
                        done({
                            result: result
                        });
                    } catch (e) {
                        done({
                            error: e.message || e
                        });
                    }
                };
 
                window.$ST.check();
 
            }, DEBUG_ME, contextGlobals).then(function (ret) {
                var value = ret.value,
                    loadST = value.loadST,
                    result = value.result,
                    error = value.error;
 
                if (DEBUG_ME) debugger
 
                if (loadST && retry) {
                    me._loadST().then(function () {
                        me._checkGlobalLeaks(done, contextGlobals, false);
                    }, function (err) {
                        done({
                            results: [{
                                passed: false,
                                message: 'Global leaks check threw an error: ' + (err || err.message)
                            }]
                        });
                    });
                    return;
                }
 
                // NOTE: done must return what Base.checkGlobalLeaks expects, currently: 
                // { results: [<expectations results>], addedGlobals: [<string property names>] } 
                if (result) {
                    done(result);
                } else if (error) {
                    done({
                        results: [{
                            passed: false,
                            message: 'Global leaks check threw an error: ' + error
                        }]
                    });
                } else {
                    done({
                        results: [{
                            passed: false,
                            message: 'Global leaks check returned no results or error.'
                        }]
                    });
                }
            },function (err) {
                if (DEBUG_ME) debugger
                done({
                    results: [{
                        passed: false,
                        message: 'Global leaks check threw an error: ' + err.message || err
                    }]
                });
            });
        },
 
        _loadST: function () {
            if (ST.LOGME) console.log('WebDriver._loadST()');
 
            var me = this,
                driver = me.driver,
                promises = [],
                files = [],
                errors = [];
 
            if (me.loadingST) {
                return me.loadingST;
            }
            me.loadingST = new Promise(function (resolve, reject) {
                me.initRecorderModal().then(function (ret) {
                    for (var file in me._STContent) {
                        files.push(file);
                    }
 
                    var _load = function (i) {
                        // exit recursion 
                        if (> files.length - 1) {
                            if (errors.length > 0) {
                                me.loadingST = null;
                                reject(errors);
                            } else {
                                var tmpOptions = {};
                                ST.apply(tmpOptions, ST.options);
                                // clear out globals and globalPatterns since we let the target manage it's own. 
                                tmpOptions.globalPatterns = {};
                                tmpOptions.globals = {};
                                
                                me.driver.executeAsync(function (testOptions, done) {
                                    ST.setupOptions(testOptions); // results in ST.initGlobals() being called. 
 
                                    if (Ext && Ext.onReady) { // this is an ext page 
                                        if (ST.LOGME) console.log('setup Ext.onReady callback');
                                        Ext.onReady(function () {
                                            if (ST.LOGME) console.log('Ext.onReady callback is called');
 
                                            ST.initExtJS(); // in case the _beforereadyhandler didn't fire 
                                            // and the delayed call didn't fire at the right time either. 
 
                                            if (ST.LOGME) console.log('after initExtJs called, ST.ready.blocks=' + ST.ready.blocks);
 
                                            done();
                                        });
                                    } else {
                                        if (ST.LOGME) console.log('no Ext and Ext.onReady so assume NOT an ext page and return');
 
                                        done(); // this is a non ext page 
                                    }
                                }, tmpOptions).then(function () {
                                    me.loadingST = null;
                                    resolve();
                                }, function () {
                                    me.loadingST = null;
                                    reject();
                                });
                            }
                            return;
                        }
 
                        var file = files[i],
                            content = me._STContent[file];
 
                        driver.executeAsync(function (file, content, done) {
                            // IN-BROWSER-BEGIN 
                            var insert = function () {
                                try {
                                    var script = document.createElement('script');
                                    script.innerHTML = content;
                                    script.setAttribute('file', file);
                                    document.body.appendChild(script);
 
                                    done({
                                        file: file,
                                        loaded: true
                                    });
                                } catch (e) {
                                    done({
                                        file: file,
                                        error: e
                                    });
                                }
                            };
 
                            if (document.readyState === 'complete') {
                                insert();
                            } else {
                                document.addEventListener('readystatechange', function () {
                                    if (document.readyState === 'complete') {
                                        insert();
                                    }
                                });
                            }
                            // IN-BROWSER-END 
                        }, file, content)
                            .then(function (ret) {
                                if (ret.value && ret.value.loaded) {
                                    if (ST.LOGME) console.log('LOADED script:' + ret.value.file);
                                    _load(+ 1);
                                } else {
                                    if (ST.LOGME) console.log('script:' + ret.value.file + ', appendChild error=', error);
                                    errors.push(ret.value);
                                    me.loadingST = null;
                                    reject(errors);
                                }
                            }, function (err) {
                                if (ST.LOGME) console.log('script:' + file + ', execution err='+JSON.stringify(err));
                                me.loadingST = null;
                                reject(err);
                            });
                    };
 
                    _load(0); // start the recursion 
                }).then(function () {
                    me.dismissRecorderModal();
                });
            });
            return me.loadingST;
        },
 
        initRecorderModal: function () {
            var me = this;
 
            return me.driver.execute(function () {
                // IN-BROWSER BEGIN 
                var modal = document.getElementById('StudioModal'),
                    modalDialog, modalText,
                    _text = 'Sencha Test Event Recorder is loading, this message will be dismissed when ready to record.';
 
                if (!modal) {
                    modal = document.createElement('div');
                    modalDialog = document.createElement('div');
                    modalText = document.createElement('span');
 
                    modal.id = 'StudioModal';
                    modal.style.display = 'block';
                    modal.style.position = 'fixed';
                    modal.style.zIndex = '1';
                    modal.style.top = '0';
                    modal.style.left = '0';
                    modal.style.width = '100%';
                    modal.style.height = '100%';
                    modal.style.paddingTop = '100px';
                    modal.style.backgroundColor = 'rgb(0,0,0)';
                    modal.style.backgroundColor = 'rgba(0,0,0,0.4';
 
                    modalDialog.style.margin = 'auto';
                    modalDialog.style.position = 'absolute';
                    modalDialog.style.top = '10%';
                    modalDialog.style.left = '10%';
                    modalDialog.style.width = '80%';
                    modalDialog.style.height = '100px';
                    modalDialog.style.padding = '20px';
                    modalDialog.style.borderWidth = '2px';
                    modalDialog.style.borderStyle = 'solid';
                    modalDialog.style.borderColor = '#025B80';
                    modalDialog.style.backgroundColor = '#FFFFFF';
                    modalDialog.style.overflow = 'auto';
 
                    modalText.innerHTML = '<p>' + _text + '</p>';
                    modalText.style.backgroundColor = '#FFFFFF';
                    modalText.style.overflow = 'auto';
                    modalText.style.wordWrap = 'normal';
                    modalText.style.textAlign = 'center';
 
                    modalDialog.appendChild(modalText);
                    modal.appendChild(modalDialog);
                    document.body.appendChild(modal);
                }
                // IN-BROWSER END 
            });
        },
 
        dismissRecorderModal: function () {
            var me = this;
 
            return me.driver.execute(function () {
                // IN-BROWSER BEGIN 
                var modal = document.getElementById('StudioModal');
                if (modal) {
                    modal.style.display = 'none';
                    while (modal.firstChild) {
                        modal.removeChild(modal.firstChild);
                    }
                    document.body.removeChild(modal);
                }
                // IN-BROWSER END 
            });
        },
 
        cleanup: function () {
            if (ST.LOGME) console.log('WebDriver cleanup()');
        },
 
        /**
         * Close the browser and shutdown the driver.
         *
         * When creating a WebDriver context manually in a test the stop method should be called in an
         * afterEach or afterAll method.
         */
        stop: function (resolve, reject) {
            var me = this, logPromise;
 
            if (ST.LOGME) console.log('WebDriver stop()');
            if (ST.LOGME) console.trace();
 
            if (!me.isRecording) {
                if (me.driver && me.LOGS_SUPPORTED) {
                    me.logTimer && clearInterval(me.logTimer);
                    logPromise = me.getLogs();
                    logPromise.then(function (ret) {
                        if (ST.LOGME) console.log('WebDriver.stop, logPromise resolve, call driver.end(), ret=',ret);
                        me.driver.end().then(resolve, reject);
                    }, function (err) {
                        if (ST.LOGME) console.log('WebDriver.stop, logPromise reject, call driver.end(), err=',err);
                        me.driver.end().then(resolve, reject);
                    });
                } else {
                    if (ST.LOGME) console.log('WebDriver.stop, no logPromise, call driver.end()');
                    me.driver.end().then(resolve, reject);
                }
            }
        },
 
        onEnd: function (resolve) {
            if (ST.LOGME) console.log('WebDriver onEnd()');
            if (ST.LOGME) console.trace();
            resolve();
        },
 
        startRecording: function () {
            var me = this,
                driver = me.driver;
 
            me.isRecording = true;
 
            driver.execute(function () {
                // IN-BROWSER-BEGIN 
                ST.defaultContext.startRecording();
                // IN-BROWSER-END 
            }).then(function (ret) {
                    if (ST.LOGME) console.log('WebDriver.startRecording: recorder started in target browser.');
            },
                function (err) {
                    // TODO handle recorder errors 
                    if (ST.LOGME) console.log('WebDriver.startRecording: Problem loading ST.', err);
            });
        },
 
        stopRecording: function () {
            var me = this,
                driver = me.driver;
 
            if (driver) {
                if (me.isRecording) {
                    me.isRecording = false;
                    ST.sendMessage('recordingStopped');
                }
                driver.end();
            }
        },
 
        execute: function (playable, fn, resolve, reject, retry) {
            var me = this,
                driver = me.driver,
                ser = me._serialize(playable);
 
            if (typeof retry === 'undefined') {
                retry = true;
            }
 
            driver.executeAsync(function (event, fnstring, debug, done) {
                // IN-BROWSER-BEGIN 
                if (typeof ST === 'undefined') {
                    done({
                        loadST: true
                    });
                    return;
                }
 
                ST.DEBUG = debug;
 
                window.$ST = window.$ST || {};
                window.$ST.exec = function () {
                    if (ST.DEBUG) debugger
 
                    eval('var fn = '+fnstring);
 
                    var playable = ST.defaultContext.createPlayable(event),
                        future = playable.future,
                        value = future._value(),
                        result;
 
                    try {
                        result = fn.call(playable, value);
                        done({
                            result: result
                        });
                    } catch (e) {
                        done({
                            error: e.message || e
                        });
                    }
                };
 
                window.$ST.exec();
                // IN-BROWSER-END 
            }, ser, fn.toString(), DEBUG_ME).then(function (ret) {
                if (DEBUG_ME) debugger
                if (ret.value && ret.value.error) {
                    reject(ret.value.error);
                } else if (ret.value.loadST) {
                    me._loadST().then(function () {
                        me.execute(playable, fn, resolve, reject, false);
                    }, reject);
                } else {
                    resolve(ret.value.result);
                }
            }, function (err) {
                if (DEBUG_ME) debugger
                reject(err);
            });
        },
 
        getUrl: function (fn) {
            this.driver.getUrl().then(fn);
        },
 
        getTitle: function (fn) {
            this.driver.getTitle().then(fn);
        },
 
        url: function (url, done) {
            var isFragment;
 
            url = new ST.Url(url);
            done = typeof done === 'function' ? done : ST.emptyFn;
            isFragment = url.isOnlyHash();
            url = isFragment ? url.getHash() : url.get();
 
            this.driver.execute(function (url, isFragment) {
                if (isFragment) {
                    window.location.hash = url;
                } else {
                    window.location = url;
                }                
            }, url, isFragment).then(done);
        },
        
        setViewportSize: function (width, height, done) {
            this.driver.setViewportSize({
                width: width,
                height: height
            }).then(done);
        },
        
        _screenshot: function (name, done) {
            var me = this;
 
            return me.driver.saveScreenshot().then(function (screenshot) {
                return me._compareScreenshot(name, me._arrayBufferToBase64(screenshot));
            }).then(function (comparison) {
                done(null, comparison);
            }, function (err) {
                done(err, null);
            });
        },
        
        _arrayBufferToBase64: function (buffer) {
            var binary = '',
                bytes = new Uint8Array(buffer),
                len = bytes.byteLength,
                i;
            
            for (= 0; i < len; i++) {
                binary += String.fromCharCode(bytes[i]);
            }
            
            return window.btoa(binary);
        },
        
        _compareScreenshot: function (name, base64Png) {
            var me = this;
            return new Promise(function (resolve, reject) {
                ST.system.screenshot({
                    data: base64Png,
                    name: name
                }, function (comparison, err) {
                    if (err) {
                        reject(err);
                    } else {
                        resolve(comparison);
                    }
                });
            })
        },
 
        // in init() we sort of monkey-patch our impl over webdrivers... to fixed 
        // non-mobile center move when offsets are not provided 
        // this code based on webdriverio 3.4.0 lib/commands/moveToObject.js 
        _moveToObject: function (selector, xoffset, yoffset) {
            /**
             * check for offset params
             */
            var hasOffsetParams = true;
            if (typeof xoffset !== 'number' && typeof yoffset !== 'number') {
                hasOffsetParams = false;
            }
 
            if (this.isMobile || hasOffsetParams) {
                return this.moveToObject(selector, xoffset, yoffset)
            }
 
            return this.element(selector).then(function (element) {
                return this.elementIdSize(element.value.ELEMENT).then(function (size) {
                    return this.elementIdLocation(element.value.ELEMENT).then(function (location) {
                        return { element: element, size: size, location: location };
                    });
                });
            }).then(function (res) {
                var _xoffset = (res.size.value.width / 2);
                var _yoffset = (res.size.value.height / 2);
                var x = res.location.value.x;
                var y = res.location.value.y;
 
                if (hasOffsetParams) {
                    _xoffset = xoffset
                    _yoffset = yoffset
                }
 
                return this.moveToObject(selector, _xoffset, _yoffset)
            });
        }
    });
 
}()) // scope for DEBUG_ME - file scope