    /*

    Siesta 5.3.0
    Copyright(c) 2009-2019 Bryntum AB
    https://bryntum.com/contact
    https://bryntum.com/products/siesta/license

    */
    Role('Siesta.Recorder.Role.CanRecordMouseMove', {

        does        : [
            Siesta.Util.Role.CanCalculatePageScroll,
            Siesta.Recorder.Role.CanSwallowException
        ],

        has : {
            recordMouseMove         : true,

            // used by RootCause, position in page coordinates
            cursorPosition          : Joose.I.Array,

            // used by RootCause (probably not needed anymore)
            lastMoveTimestamp       : null,

            // used by RootCause
            lastMouseMoveEvent      : null
        },

        override : {

            initialize : function () {
                this.SUPERARG(arguments)

                this.onBodyMouseMove        = this.safeBind(this.onBodyMouseMove);
                this.onBodyMouseOver        = this.safeBind(this.onBodyMouseOver);
                this.onBodyMouseOut         = this.safeBind(this.onBodyMouseOut);
            },

            onStart : function () {
                this.SUPERARG(arguments);

                var win     = this.window;
                var doc     = win.document;
                var body    = doc.body

                if (this.recordMouseMove) {
                    // Need to also observe `mouseover` since it's fired before `mousemove` in case there is a crash
                    // in `mousemove` handler we need to know the exact cursor position
                    body.addEventListener('mouseover', this.onBodyMouseOver, true);
                    body.addEventListener('mouseout', this.onBodyMouseOut, true);
                    body.addEventListener('mousemove', this.onBodyMouseMove, true);
                }
            },

            onStop : function () {
                this.SUPERARG(arguments);

                var win     = this.window;
                var doc     = win.document;
                var body    = doc.body

                if (this.recordMouseMove) {
                    body.removeEventListener('mouseover', this.onBodyMouseOver, true);
                    body.removeEventListener('mouseout', this.onBodyMouseOut, true);
                    body.removeEventListener('mousemove', this.onBodyMouseMove, true);

                    this.lastMouseMoveEvent = null
                }
            }
        },

        methods : {

            saveMouseMoveData : function (event) {
                var me                  = this

                me.lastMoveTimestamp    = new Date() - 0

                me.cursorPosition[ 0 ]  = me.viewportXtoPageX(event.clientX, me.window)
                me.cursorPosition[ 1 ]  = me.viewportYtoPageY(event.clientY, me.window)

                me.lastMouseMoveEvent   = event
            },

            onBodyMouseMove        : function (e) {
                // Skip test playback events and mouse moves in frames
                if ((this.ignoreSynthetic && e.synthetic) || e.target.ownerDocument !== this.window.document) return;

                this.saveMouseMoveData(e)

                this.processBodyMouseMove(e)
            },

            processBodyMouseMove : function (e) {
            },

            onBodyMouseOver : function (e) {
                // Skip test playback events and mouse moves in frames
                if ((this.ignoreSynthetic && e.synthetic) || e.target.ownerDocument !== this.window.document) return;

                this.saveMouseMoveData(e)

                this.processBodyMouseOver(e)
            },

            processBodyMouseOver : function (e) {
            },

            onBodyMouseOut : function (e) {
                // Skip test playback events and mouse moves in frames
                if ((this.ignoreSynthetic && e.synthetic) || e.target.ownerDocument !== this.window.document) return;

                this.saveMouseMoveData(e)

                this.processBodyMouseOut(e)
            },

            processBodyMouseOut : function (e) {
            },

            targetToPathPoint : function (target) {
                var pathPoint   = [ target.getTargetAsQueryString() ]

                if (target.getTarget().offset) pathPoint.push.apply(pathPoint, target.getTarget().offset)

                pathPoint.xy    = target.getTargetByType('xy').target

                return pathPoint
            },

            addPathPoint : function (pathPoints, newPathPoint) {
                // early exit, in case new path point is query-based
                if (newPathPoint.length === 3) {
                    pathPoints.push(newPathPoint)
                    return
                }

                var lastPathPoint       = pathPoints[ pathPoints.length - 1 ]

                // if new path point is a absolute point (which will be the case for the `useXY` arg
                // of `addMoveCursorAction` enabled then covert it to relative
                if (newPathPoint.length === 1 && this.typeOf(newPathPoint[ 0 ]) == 'Array') {
                    var relativePoint   = [
                        newPathPoint[ 0 ][ 0 ] - lastPathPoint.xy[ 0 ],
                        newPathPoint[ 0 ][ 1 ] - lastPathPoint.xy[ 1 ]
                    ]

                    // do nothing
                    if (relativePoint[ 0 ] === 0 && relativePoint[ 1 ] === 0) return

                    relativePoint.xy    = newPathPoint[ 0 ]

                    pathPoints.push(relativePoint)
                } else
                    pathPoints.push(newPathPoint)
            },

            // this method merges several consequent calls to it to `moveCursorAlongPath` action
            addMoveCursorAction : function (event, targetOverride, useXY) {
                if (!(event instanceof Siesta.Recorder.Event)) event = Siesta.Recorder.Event.fromDomEvent(event)

                var lastAction      = this.getLastAction()

                var isMouseMove     = lastAction && (lastAction.action == 'moveCursorTo' || lastAction.action == 'moveCursorAlongPath')

                if (!lastAction || !isMouseMove || !this.recordMouseMovePath) {

                    // defensive coding because of unknown: `lastAction.target` is undefined exception
                    if (lastAction && lastAction.hasTarget() && lastAction.target) {
                        var xy      = lastAction.target.getTargetByType('xy')

                        if (xy) {
                            xy      = xy.target

                            // do nothing if we already have an action with the same target point
                            // this might happen during our own tests, where we use `drag` command, which
                            // issues "mouseover" for initial and last points
                            if (event.x == xy[ 0 ] && event.y == xy[ 1 ]) return
                        }
                    }

                    var targetOptions   = this.getPossibleTargets(event, true, targetOverride, useXY)

                    this.addAction({
                        action          : 'moveCursorTo',

                        target          : targetOptions,

                        sourceEvent     : event,
                        options         : event.options
                    })
                } else {
                    var target      = new Siesta.Recorder.Target({
                        targets         : this.getPossibleTargets(event, true, targetOverride, useXY)
                    })

                    if (lastAction.action == 'moveCursorTo') {
                        lastAction.action   = 'moveCursorAlongPath'

                        lastAction.value    = [ this.targetToPathPoint(lastAction.target) ]

                        lastAction.target   = null
                    }

                    this.addPathPoint(lastAction.value, this.targetToPathPoint(target))

                    this.fireEvent('actionupdate', lastAction)
                }
            }
        }
        // eof `override`
    });