/**
 * @class ST.context.WebDriverRecorder
 * @private
 */
(function() {
 
    ST.context.WebDriverRecorder = ST.define({
        extend: ST.context.WebDriver,
 
        isMoving: false,
 
        constructor: function (config) {
            ST.context.WebDriverRecorder.superclass.constructor.call(this, config);
        },
 
        init: function () {
            var me = this;
 
            me.STLoads = 0;
            me.loadingST = false;
 
            return me.driver = ST.client = ST.webdriverio
                .remote(me.driverConfig)
                .init(me.driverConfig)
                .url(me.subjectUrl)
                .then(function () {
                    me.driver.addCommand('STmoveToObject',me._moveToObject);
                    me
                        .initBrowserInfo()
                        .then(function () {
                            me._loadST();
                        });
                })
                .saveScreenshot()
                .then(function () {
                    me.initScrollSurface().then(function () {
                        me.initResizer();
                    });
                });
        },
 
        getSelector: function (x, y) {
            var el = document.elementFromPoint(x, y),
                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 '@';
            }
        },
 
        /**
         * Primary error handler for WebDriver calls.
         *
         * WDIO work on the global error handling has improved as of 4.x: https://github.com/webdriverio/webdriverio/issues/827
         *
         * @param err
         * @private
         */
        handleWDError: function (err) {
            var me = this;
            if (err.seleniumStack) {
                if (err.seleniumStack.status == 13 || err.seleniumStack.status == 23 || err.type === 'NoSessionIdError') {
                    // Target browser was closed from under us, close accordingly 
                    me.stopRecording();
                }
            } else if (err.type === 'RuntimeError') {
                // RuntimeError is more generic, harder to determine what happened. 
                // Most likely fell in here because driver.moveToObject fails miserably... 
                // no selenium stack or obvious property to check... have to grep the message property 
                if (err.message.indexOf('UnknownError:13') !== -1) {
                    // Most likely Target browser was closed. Close accordingly 
                    // UnknownError:13 seems to match the seleniumStack.status above... curious 
                    me.stopRecording();
                }
            } else if (err.type === 'UnknownError') {
                // Most likely landed here because there was a quirk with the RuntimeError handling above. 
                if (err.status == 13 || err.status == 23) {
                    // Strangely matches the seleniumStack.status again... 
                    me.stopRecording();
                }
            } else if (err.name === 'TypeError') {
                // This happens on OSX when the target browser window is closed and then the browser is quit 
                // in a separate action (Cmd-Q). grep the message just to be sure. 
                if (err.message.indexOf('Cannot read property') !== -1) {
                    if (err.message.indexOf('of null') !== -1) {
                        // Probably the target window was closed out-of-band. 
                        me.stopRecording();
                    }
                }
            } else {
                // Leaving this here so bugs can tell us what else could cause a problem. 
                // The filtering so far is all I can seem to make happen locally. I'm sure 
                // if we are communicating with a remote Selenium Grid we could get different things. 
                // 
                // https://github.com/webdriverio/webdriverio/blob/master/lib/helpers/constants.js 
                // search for ERROR_CODES 
                debugger;
            }
        },
 
        initListeners: function () {
            // handlers here are simply pass-throughs from the canvas to the target 
            // which most likely we do not want recorded 
            var me = this,
                magicCanvas = me.magicCanvas = document.getElementById('magicCanvas');
 
            magicCanvas.onmousemove = function (e) {
                if (!me.isClosing) {
                    if (!me.isMoving) {
                        me.isMoving = true;
                        me.STLoaded().then(function () {
                            me.driver
                                .moveToObject('#ST_scroll_surface', e.clientX, e.clientY)
                                .then(function () {
                                    me.isMoving = false;
                                    me.mousemoveFailures = 0;
                                });
                        }).catch(function () {
                            me._loadST().then(function () {
                                me.initScrollSurface();
                            });
                        });
                    } else {
                        setTimeout(function () {
                            me.isMoving = false;
                        }, 100);
                    }
                }
            }.bind(me);
 
            magicCanvas.oncontextmenu = function (e) {
                me.driver.execute(me.getSelector, e.x, e.y).then(function (_selector) {
                    me.driver.rightClick(_selector.value);
                }).catch(function (err) {
                    me.handleWDError(err);
                });
            };
        },
 
        initResizer: function () {
            var me = this,
                remote = require('electron').remote,
                ipcRenderer = require('electron').ipcRenderer,
                screen = require('electron').screen,
                sandboxName = 'Sencha Test Sandbox', // TODO centralize access to the name among code 
                browserName = me.driverConfig.desiredCapabilities.browserName,
                browserTitle, browserWindow, browserDisplay,
                recorderWindow, recorderNativeWindow,
                applyScaleFactor = (me.driverConfig.desiredCapabilities.browserName === 'internet explorer'
                || me.driverConfig.desiredCapabilities.browserName === 'MicrosoftEdge'),
                lastViewportW, lastViewportH,
                lastHandleX, lastHandleY;
 
            // TODO HACK to translate selenium browserName to something the OS can use to find the window 
            // from https://github.com/SeleniumHQ/selenium/wiki/DesiredCapabilities 
            // {android|chrome|firefox|htmlunit|internet explorer|iPhone|iPad|opera|safari} 
            // and by inspection on OSX and Window (not Linux :() I make the translations below 
            if (browserName == 'chrome') {
                browserName = 'Google Chrome'
            } else if (browserName == 'MicrosoftEdge') {
                browserName = 'Microsoft Edge'
            } else if (browserName == 'internet explorer') {
                browserName = 'Internet Explorer'
            } else if (browserName == 'firefox') {
                browserName = 'Mozilla Firefox'
            }
 
            if (!me.recorderWindow) {
                recorderWindow = me.recorderWindow = remote.getCurrentWindow();
            } else {
                recorderWindow = me.recorderWindow;
            }
 
            if (ST.os.is.Win32) {
                // TODO explicit reference between the set title and this title to search 
                if (ST.LOGME) ST.log('look for recorderNativeWindow')
                recorderNativeWindow = ST.nativewindow.findWindow(sandboxName) // no app name on windows! 
                if (ST.LOGME) ST.log('found recorderNativeWindow=' + recorderNativeWindow)
 
                recorderWindow.on('focus', function () {
                    if (ST.LOGME) ST.log('recorderWindow focus event, bringToTop(browserWindow)')
                    ST.nativewindow.bringToTop(browserWindow)
                    if (ST.LOGME) ST.log('recorderWindow focus event, bringToTop(recorderNativeWindow)')
                    ST.nativewindow.bringToTop(recorderNativeWindow)
                })
            }
 
            me.resizer = setInterval(() => {
                if (!me.isClosing) {
                    var viewportW, viewportH,
                        handleW, handleH,
                        handleX, handleY;
 
                    me.driver.getViewportSize().then((viewportSize) => {
                        if (ST.LOGME) ST.log('viewportSize='+JSON.stringify(viewportSize))
 
                        viewportW = viewportSize.width;
                        viewportH = viewportSize.height;
 
                        if (viewportW !== lastViewportW || viewportH !== lastViewportH) {
                            lastViewportW = viewportW;
                            lastViewportH = viewportH;
                            recorderWindow.setSize(viewportW, viewportH);
                        }
                        return me.driver.title();
                    }).then((title) => {
                        var title = title.value,
                            browserOnTop, sandboxOnTop,
                            titleChanged = false
 
                        if (browserTitle != title) {
                            titleChanged = true
                            browserTitle = title
                        }
 
                        if (ST.os.is.MacOS && titleChanged) {
                            ST.log('call updateRecorderFocusManager in main process via ipc')
                            var result = ipcRenderer.sendSync('updateRecorderFocusManager', {
                                browserTitle: browserTitle,
                                browserName: browserName
                            })
                        }
 
                        if (ST.os.is.Win32) {
                            // refresh the browserWindow handle if title has changed or if it isn't found yet 
                            if (titleChanged || !browserWindow) {
                                if (ST.LOGME) ST.log('look for window with windowName='+browserTitle+' and appName='+browserName)
                                browserWindow = ST.nativewindow.findWindow(browserTitle,browserName)
                                if (ST.LOGME) ST.log('found browserWindow='+browserWindow)
                            }
 
                            browserOnTop = ST.nativewindow.isTopWindow(browserWindow)
                            if (ST.LOGME) ST.log('browserOnTop=' + browserOnTop)
 
                            // TODO switch to OS-based event driven process to watch for browser-focus 
                            // to bring recorder to top 
                            // NOTE case of sandboxOnTop is handled by recorderWindow.on('focus') above 
                            if (browserOnTop) {
                                if (ST.LOGME) ST.log('send sandbox to top')
                                ST.nativewindow.bringToTop(recorderNativeWindow)
                            } else {
                                if (ST.LOGME) ST.log('sending browser to bottom')
                                ST.nativewindow.sendToBottom(browserWindow)
                                if (ST.LOGME) ST.log('sending recorder to bottom')
                                ST.nativewindow.sendToBottom(recorderNativeWindow)
                            }
                        }
 
                        return me.driver.windowHandleSize();
                    }).then(function (handleSize) {
                        if (ST.LOGME) ST.log('handleSize='+JSON.stringify(handleSize))
 
                        handleW = handleSize.value.width;
                        handleH = handleSize.value.height;
 
                        return me.driver.windowHandlePosition();
                    }).then((handlePosition) => {
                        if (ST.LOGME) ST.log('handlePosition='+JSON.stringify(handlePosition.value))
 
                        var scaleFactor, x, y;
 
                        handleX = handlePosition.value.x;
                        handleY = handlePosition.value.y;
 
                        if (handleX !== lastHandleX || handleY !== lastHandleY) {
                            lastHandleX = handleX;
                            lastHandleY = handleY;
 
                            browserDisplay = screen.getDisplayMatching({
                                x: handleX,
                                y: handleY,
                                width: handleW,
                                height: handleH
                            });
                            if (ST.LOGME) ST.log('browserDisplay='+JSON.stringify(browserDisplay))
                            scaleFactor = browserDisplay.scaleFactor;
 
                            if (applyScaleFactor && scaleFactor != 1) {
                                // scale all the values because WINDOWS 
                                handleX = Math.floor(handleX / scaleFactor);
                                handleY = Math.floor(handleY / scaleFactor);
                                handleW = Math.floor(handleW / scaleFactor);
                                handleH = Math.floor(handleH / scaleFactor);
                            }
 
                            x = Math.floor(handleX + (handleW - viewportW) / 2);
                            y = handleY + handleH - viewportH;
 
                            // NOTE setPosition seems to cause EVENT_SYSTEM_FOREGROUND event on windows... 
                            if (ST.os.is.Win32) {
                                y = y - (10 - (scaleFactor * 2)) // TODO compute from data instead of guessing 
                            }
                            if (ST.LOGME) ST.log('setPosition('+x+','+y+')')
                            recorderWindow.setPosition(x, y);
 
                            if (viewportW !== lastViewportW || viewportH !== lastViewportH) {
                                lastViewportW = viewportW;
                                lastViewportH = viewportH;
                                if (ST.LOGME) ST.log('setSize('+viewportW+','+viewportH+')')
                                recorderWindow.setSize(viewportW, viewportH);
                            }
                        }
 
                        if (!recorderWindow.__shown) {
                            recorderWindow.show();
                            recorderWindow.__shown = true;
                        }
 
                        me.STLoaded().catch(function () {
                            me._loadST().then(function () {
                                me.initScrollSurface();
                            });
                        });
                    }).catch(function (err) {
                        me.handleWDError(err);
                    });
                }
            }, 1000);
        },
 
        /**
         * @private
         * When you use moveToObject() with the body element, webdriver will reset any scroll positions on the
         * document, which doesn't make scrolling very nice at all. As a workaround, we'll inject a fixed element
         * that will fit itself to the size of the document and act as the "surface" on which the mousemovements are tracked
         * This should let us avoid the scroll reset issue while still tracking the mousemovements which are necessary for scroll
         */
        initScrollSurface: function () {
            var me = this;
 
            if (me.isScrollSurfaceIniting) return;
            me.isScrollSurfaceIniting = true;
 
            return me.driver.execute(function () {
                // IN-BROWSER BEGIN 
                var el = document.getElementById('ST_scroll_surface'),
                    h, w;
                w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
                h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
 
                if (!el) {
                    el = document.createElement('DIV');
                    el.id = 'ST_scroll_surface';
                    el.style.height = h + 'px';
                    el.style.width = w + 'px';
                    el.style.position = 'fixed';
                    el.style.top = 0;
                    el.style.left = 0;
                    //el.style.backgroundColor = 'yellowgreen'; // useful for debugging ;P 
                    el.style.zIndex = -1000000000;
 
                    document.body.appendChild(el);
                }
                window.addEventListener('resize', function () {
                    var el = document.getElementById('ST_scroll_surface'),
                        h, w;
 
                    w = Math.max(document.documentElement.clientWidth, window.innerWidth || 0);
                    h = Math.max(document.documentElement.clientHeight, window.innerHeight || 0);
 
                    el.style.height = h + 'px';
                    el.style.width = w + 'px';
                });
                // IN-BROWSER END 
            }).then(function () {
                me.isScrollSurfaceIniting = false;
                me.initListeners();
                return Promise.resolve();
            });
        },
 
        _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',
                    'event/Event.js',
                    'event/GestureQueue.js',
                    'event/wgxpath.install.js',
                    'Locator.js',
                    'locator/Strategy.js',
                    'event/Driver.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;
        },
 
        scrollSurfaceLoaded: function () {
            var me = this;
 
            return new Promise(function (resolve, reject) {
                me.driver.element('#ST_scroll_surface').then(function (ret) {
                    if (ret.value) {
                        reject(ret);
                    } else {
                        resolve(ret); // ST already loaded in target 
                    }
                }).catch(function (err) {
                    me.handleWDError(err);
                    reject();
                });
            });
        },
 
        /**
         * Starts the {@link ST.event.Recorder event recorder}. Once this method is called
         * all further test execution is halted.
         *
         * This method is typically injected automatically by Sencha Test Studio when using
         * its Event Recorder and is therefore rarely called directly.
         * @method startRecording
         * @member ST
         */
        startRecording: function () {
            var me = this,
                recorder = me.recorder || (me.recorder = new ST.event.MagicCanvasRecorder());
 
            me.isRecording = true;
            me.isClosing = false;
            recorder.throttle = 1;
 
            try {
                recorder.on({
                    scope: ST,
                    add: function (recorder, events) {
                        ST.sendMessage({
                            type: 'recordedEvents',
                            events: events
                        });
                    },
                    start: function () {
                        ST.sendMessage({
                            type: 'recordingStarted'
                        });
                    }
                });
 
                recorder.start();
            } catch (err) {
                if (recorder) {
                    console.log(recorder);
                }
 
                console.error(err.message || err);
                console.error(err.stack || err);
            }
        },
 
        stopRecording: function () {
            var me = this,
                promise = Promise.resolve();
 
            clearInterval(me.resizer);
 
            // There is a timeout issue with WebDriver: calls can take some time to timeout, 
            // so ignore if we are already trying to close 
            if (me.isClosing) return;
            me.isClosing = true;
 
            if (me.recorder) me.recorder.stop();
 
            if (me.magicCanvas && me.magicCanvas.onmousemove) me.magicCanvas.onmousemove = null;
 
            return me.driver.end().then(function () {
                ST.sendMessage('recordingStopped');
            }).catch(function (err) {
                ST.sendMessage('recordingStopped');
            });
        },
 
        /**
         * Checks to see if Sencha Test is loaded in the target browser
         * @returns {Promise}
         * @private
         */
        STLoaded: function () {
            var me = this;
 
            return new Promise(function (resolve, reject) {
                me.driver.execute(function () {
                    return (typeof ST !== 'undefined');
                }).then(function (ret) {
                    if (ret.value) {
                        resolve(ret); // ST already loaded in target 
                    } else {
                        reject(ret);
                    }
                }).catch(function (err) {
                    me.handleWDError(err);
                    reject();
                });
            });
        },
 
        /**
         * Wraps the event intercepted by the Magic Canvas into a Sencha Test Event, to be
         * inserted into the target by the recorder.
         * @returns {ST.Event}
         * @private
         */
        wrapEvent: function (ev) {
            var me = this,
                eventData = new ST.event.Event(ev, [], new Date().getTime()),
                point;
 
            // Get the coordinates of the mouse 
            return me._translateCoordinates(ev).then(function (_point) {
                point = _point;
                return me.STLoaded().then(function (ret) {
                    // Wrap the event 
                    return me.driver.execute(function (_point, _ev) {
                        // IN-BROWSER BEGIN 
                        var _targets = [],
                            _strategy = new ST.locator.Strategy(), // TODO include other strategies from Recorder 
                            _element = document.elementFromPoint(_point.x, _point.y) || document.body,
                            _id = new Date().getTime(),
                            _attr = attr = 'data-senchatestid',
                            _event;
 
                        // if we have a keycode, we'll assume it's a focused input element 
                        if (_ev.keyCode) {
                            _element = document.activeElement;
                        }
                        // let's make sure that we NEVER allow ST_scroll_surface as a valid target; 
                        // it will NEVER be during playback :) 
                        if (_element.id && _element.id === 'ST_scroll_surface') {
                            _element = document.body;
                        }
 
                        _strategy.locate(_element, _targets, _ev);
                        _event = new ST.event.Event(_ev, _targets);
                        _event.webElement = _element;
                        // tag element with a permanent selector 
                        // if we don't already have an attribute, add it with the value 
                        if (!_element.hasAttribute(_attr)) {
                            _element.setAttribute(_attr, _id);
                        }
                        // otherwise, we'll just use the one that was previously assigned 
                        else {
                            _id = _element.getAttribute(_attr);
                        }
                        // compose the selector, job done!! 
                        _event.generatedSelector = "[" + _attr + "='" + _id + "']";
 
                        return _event.serialize();
                        // IN-BROWSER END 
                    }, point, eventData.serialize());
                }).catch(function (err) {
                    // Most likely a page navigation occurred. Reload ST in the target. 
                    return me._loadST().then(function () {
                        return me.initScrollSurface().then(function (ret) {
                            // Return the event to the recorder anyway since it most likely 
                            // triggered navigation. The event that triggers this 
                            // is usually Enter or spacebar on a form/button. 
                            return Promise.resolve({
                                value: eventData.serialize()
                            });
                        });
                    });
                });
            }).catch(function (err) {
                me.handleWDError(err);
            });
        },
 
        /**
         * Translates coordinates of an event in the MagicCanvas to the target browser under test
         * @param ev
         * @private
         */
        _translateCoordinates: function (ev) {
            var me = this,
                screenX = ev.screenX,
                screenY = ev.screenY,
                handlePosX, handlePosY,
                handleSizeW, handleSizeH,
                viewportPosX, viewportPosY,
                viewportSizeW, viewportSizeH,
                x, y;
 
            //TODO handle caching of handleSizeW/H 
            return me.driver.windowHandlePosition()
                .then(function (position) {
                    handlePosX = position.value.x;
                    handlePosY = position.value.y;
 
                    return me.driver.windowHandleSize();
                })
                .then(function (size) {
                    handleSizeW = size.value.width;
                    handleSizeH = size.value.height;
 
                    return me.driver.getViewportSize();
                })
                .then(function (size) {
                    viewportSizeW = size.width;
                    viewportSizeH = size.height;
                    viewportPosX = handlePosX + Math.floor((handleSizeW - viewportSizeW) / 2);
                    viewportPosY = handlePosY + (handleSizeH - viewportSizeH);
 
                    x = screenX - viewportPosX;
                    y = screenY - viewportPosY;
 
                    return {x: x, y: y};
                })
                .catch(function (err) {
                    me.handleWDError(err);
                });
        }
    });
}());