/* eslint-env es6, node */
/**
 * @class ST.context.WebDriverRecorder
 * @private
 */
var async = require('async');
(function() {
    var logger = ST.logger.forClass('context/WebDriverRecorder'),
        debug = ST.debug;
 
    ST.context.WebDriverRecorder = ST.define({
        extend: ST.context.WebDriver,
 
        constructor: function (config) {
            logger.trace('.constructor');
            ST.context.WebDriverRecorder.superclass.constructor.call(this, config);
        },
 
        init: function () {
            logger.trace('.init');
            logger.debug('Initializing WebDriverRecorder context');
            var me = this,
                path = require('path'),
                fs = require('fs'),
                parentDir = path.join(__dirname, '..'),
                driver;
 
            me.STLoads = 0;
            me._addRemoteHelpers();
 
            logger.debug('Initializing native window module');
            ST.nativewindow = require('native-window');
 
            logger.debug('Initializing WebdriverIO');
            driver = me.driver = ST.client = ST.webdriverio
                .remote(me.driverConfig)
                .init();
 
            driver.addCommand('STmoveToObject', me._moveToObject);
 
            return driver
                .url(me.subjectUrl)
                .then(() => {
                    return me.initBrowserInfo();
                })
                .then(() => {
                    return me._loadST();
                })
                .then(() => {
                    return me.initScrollSurface();
                })
                .then(() => {
                    return me.initResizer();
                });
        },
 
        _STFiles: [
            'init.js',
            'logger.js',
            'debug.js',
            'supports.js',
            'base.js',
            'Version.js',
            'common.js',
            'cache.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'
        ],
 
        _addRemoteHelpers: function () {
            logger.trace('._addRemoteHelpers');
            Object.assign(this.remoteHelpers, {
 
                getSelector: {
                    name: 'getSelector',
                    fn: 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 '@';
                        }
                    }
                },
 
                initScrollSurface: {
                    name: 'initScrollSurface',
                    fn: 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);
 
                        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';
                        });
                    }
                },
 
                wrapEvent: {
                    name: 'wrapEvent',
                    // cache: true,
                    // minify: true,
                    serializeArgs: true,
                    parseResult: true,
                    fn: 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 = '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 = ST.cache.add(_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 + "']";
 
                        _event = _event.serialize();
                        return JSON.stringify(_event);
                    }
                },
 
                STLoaded: {
                    name: 'STLoaded',
                    cache: false,
                    minify: false,
                    fn: 'return (typeof ST !== "undefined");'
                }
            });
            
            
        },
 
        /**
         * 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) {
            logger.trace('.handleWDError');
            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 () {
            logger.trace('.initListeners');
            // 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'),
                canvasLogger = logger.forClass('context/WebDriverRecorder.MagicCanvas'),
                lastX, lastY,
                mouseMovePromise, mouseMoveTimeout, mouseMoveResolve, mouseMoveReject;
 
            me.pendingEvents = Promise.resolve();
 
            magicCanvas.onmousemove = function (e) {
                var x = e.clientX,
                    y = e.clientY;
 
                if (me.isClosing || (x === lastX && y === lastY)) {
                    return;
                }
 
                canvasLogger.loquacious('magicCanvas', 'onmousemove');
                lastX = x;
                lastY = y;
 
                if (!mouseMovePromise) {
                    mouseMovePromise = new Promise(function (resolve, reject) {
                        mouseMoveResolve = resolve;
                        mouseMoveReject = reject;
                    })
                    me.pendingEvents = me.pendingEvents.then(() => {
                        return mouseMovePromise;
                    });
                }
 
                clearTimeout(mouseMoveTimeout);
                mouseMoveTimeout = setTimeout(function() {
                    canvasLogger.loquacious('mouseMoveTimeout');
                    me.STLoaded()
                    .then(() => {
                        canvasLogger.loquacious('driver.moveToObject()', x, y)
                        return me.driver.moveToObject('#ST_scroll_surface', x, y);
                    })
                    .then(() => {
                        me.mousemoveFailures = 0;
                        mouseMovePromise = null;
                    })
                    .then(mouseMoveResolve)
                    .catch(function () {
                        canvasLogger.loquacious('mouseMoveTimeout', 'catch');
                        mouseMovePromise = null;
                        mouseMoveReject();
                        me._loadST()
                        .then(function () {
                            return me.initScrollSurface();
                        });
                    });
                }, 100);
 
            }.bind(me);
 
            magicCanvas.oncontextmenu = function (e) {
                canvasLogger.loquacious('.oncontextmenu');
                var helperCtx = me.remoteHelpers.getSelector;
                me.executeScript(helperCtx, e.x, e.y).then(function (result) {
                    var selector = result.value;
                    me.driver.rightClick(selector);
                }).catch(function (err) {
                    me.handleWDError(err);
                });
            };
        },
 
        initResizer: function () {
            var me = this,
                resizerLogger = logger.forClass('context/WebDriverRecorder.Resizer'),
                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
                resizerLogger.babble('look for recorderNativeWindow');
                recorderNativeWindow = ST.nativewindow.findWindow(sandboxName) // no app name on windows!
                resizerLogger.babble('found recorderNativeWindow=' + recorderNativeWindow)
 
                recorderWindow.on('focus', function () {
                    resizerLogger.babble('recorderWindow focus event, bringToTop(browserWindow)')
                    ST.nativewindow.bringToTop(browserWindow)
                    resizerLogger.babble('recorderWindow focus event, bringToTop(recorderNativeWindow)')
                    ST.nativewindow.bringToTop(recorderNativeWindow)
                })
            }
 
            me.resizer = setInterval(() => {
    
                resizerLogger.babble('resizer', 'setInterval');
                if (!me.isClosing) {
                    var viewportW, viewportH,
                        handleW, handleH,
                        handleX, handleY;
 
                    me.driver.getViewportSize().then((viewportSize) => {
                        resizerLogger.babble('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,
                            titleChanged = false;
                        resizerLogger.babble('resizer')
 
                        if (browserTitle != title) {
                            titleChanged = true
                            browserTitle = title
                        }
 
                        if (ST.os.is.MacOS && titleChanged) {
                            resizerLogger.babble('Sending IPC message: updateRecorderFocus')
                            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) {
                                resizerLogger.babble('look for window with windowName='+browserTitle+' and appName='+browserName)
                                browserWindow = ST.nativewindow.findWindow(browserTitle,browserName)
                                resizerLogger.babble('found browserWindow='+browserWindow)
                            }
 
                            browserOnTop = ST.nativewindow.isTopWindow(browserWindow)
                            resizerLogger.babble('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) {
                                resizerLogger.babble('send sandbox to top')
                                ST.nativewindow.bringToTop(recorderNativeWindow)
                            } else {
                                resizerLogger.babble('sending browser to bottom')
                                ST.nativewindow.sendToBottom(browserWindow)
                                resizerLogger.babble('sending recorder to bottom')
                                ST.nativewindow.sendToBottom(recorderNativeWindow)
                            }
                        }
 
                        return me.driver.windowHandleSize();
                    }).then(function (handleSize) {
                        resizerLogger.babble('handleSize='+JSON.stringify(handleSize))
 
                        handleW = handleSize.value.width;
                        handleH = handleSize.value.height;
 
                        return me.driver.windowHandlePosition();
                    }).then((handlePosition) => {
                        resizerLogger.babble('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
                            });
                            resizerLogger.babble('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
                            }
                            resizerLogger.babble('setPosition('+x+','+y+')')
                            recorderWindow.setPosition(x, y);
 
                            if (viewportW !== lastViewportW || viewportH !== lastViewportH) {
                                lastViewportW = viewportW;
                                lastViewportH = viewportH;
                                resizerLogger.babble('setSize('+viewportW+','+viewportH+')')
                                recorderWindow.setSize(viewportW, viewportH);
                            }
                        }
 
                        if (!recorderWindow.__shown) {
                            recorderWindow.show();
                            recorderWindow.__shown = true;
                        }
                    }).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 () {
            logger.trace('.initScrollSurface');
            var me = this,
                helperCtx;
 
            if (me.isScrollSurfaceIniting) return;
            me.isScrollSurfaceIniting = true;
 
            helperCtx = me.remoteHelpers.initScrollSurface;
            return me.executeScript(helperCtx).then(function () {
                me.isScrollSurfaceIniting = false;
                me.initListeners();
                return Promise.resolve();
            });
        },
 
        scrollSurfaceLoaded: function () {
            logger.trace('.scrollSurfaceLoaded');
            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 () {
            logger.trace('.startRecording');
            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 (resolve, reject) {
            logger.trace('.stopRecording');
            var me = this;
 
            if (me._stopRecordingPromise) {
                logger.trace('.stopRecording => me._stopRecordingPromise');
                return me._stopRecordingPromise;
            }
 
            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;
 
            debug(function () {
                for (let ctxName in me.remoteHelpers) {
                    let ctx = me.remoteHelpers[ctxName];
                    if (!ctx.execCount) {
                        continue;
                    }
                    console.log('------------------ CONTEXT');
                    console.log('helper:            ' + ctxName);
                    console.log('asynchronous:      ' + !!ctx.async);
                    console.log('minified:          ' + !!ctx.miniFn);
                    console.log('pre-serialized:    ' + !!ctx.serializeArgs);
                    console.log('id:                ' + (ctx.id || 'n/a'));
                    console.log('fn:                ' + ctx.description);
                    console.log('cachingTime:       ' + (ctx.cachingTime || 'n/a'));
                    console.log('execCount:         ' + ctx.execCount);
                    console.log('execTime (total) : ' + ctx.execTime);
                    console.log('execTime (avg)   : ' + Math.floor(ctx.execTime / ctx.execCount));
                    console.log('execTime (series): ' + JSON.stringify(ctx.execTimes));
                    console.log('\n');
                }
            });
            debug();
 
            me.isRecording = false;
 
            async.series([
                function (callback) {
                    me.stop(resolve, reject);
                    callback();
                },
                function () {
                    setTimeout(function () {
                        if (me.recorderWindow) {
                            me.recorderWindow.close();
                        }
                    }, 1000);
                }
            ]);
        },
 
        /**
         * Checks to see if Sencha Test is loaded in the target browser
         * @returns {Promise}
         * @private
         */
        STLoaded: function () {
            logger.trace('.STLoaded');
            var me = this,
                helperCtx = me.remoteHelpers.STLoaded;
 
            return new Promise(function (resolve, reject) {
                me.executeScript(helperCtx)
                .then(function (ret) {
                    if (ret.value) {
                        resolve(ret); // ST already loaded in target
                    } else {
                        reject(ret);
                    }
                }).catch(function (err) {
                    logger.error((err && err.stack) || 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) {
            logger.trace('.wrapEvent');
            var me = this,
                eventData = new ST.event.Event(ev, [], new Date().getTime()),
                point, helperCtx;
 
            // Get the coordinates of the mouse
            return me._translateCoordinates(ev).then(function (_point) {
                point = _point;
                return me.STLoaded().then(function (ret) {
                    // Wrap the event
                    helperCtx = me.remoteHelpers.wrapEvent;
                    eventData = eventData.serialize();
                    return me.executeScript(helperCtx, point, eventData);
                }).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) {
            logger.trace('._translateCoordinates');
            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);
                });
        }
    });
}());