    /*

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

    */
    /**
    @class Siesta.Test.UserAgent.Touch

    This is a mixin, providing the touch events simulation functionality.
    */
    Role('Siesta.Test.UserAgent.Touch', {

        requires        : [
            'normalizeElement'
        ],

        has: {
            notSupportedWarned  : false
        },

        methods: {

            checkTouchEventsSupport : function () {
                var supports        = Siesta.Project.Browser.FeatureSupport().supports

                var root            = this.getRootTest()

                if (!supports.TouchEvents && !supports.PointerEvents && !supports.MSPointerEvents && !root.notSupportedWarned) {
                    root.notSupportedWarned = true

                    this.warn("Touch events are not supported by browser. For Chrome, you can enable them, by launching it with: --args --touch-events")
                }
            },

            /**
             * This method taps the passed target, which can be of several different types, see {@link Siesta.Test.ActionTarget}
             *
             * @param {Siesta.Test.ActionTarget} target Target for this action
             * @param {Function} callback (optional) A function to call after action.
             * @param {Object} scope (optional) The scope for the callback
             * @param {Object} options (optional) Any options that will be used when simulating the event. For information about possible
             * config options, please see: <https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent>
             * @param {Array} offset (optional) An X,Y offset relative to the target. Example: [20, 20] for 20px or
             * ["50%", "100%-2"] to click in the center horizontally and 2px from the bottom edge.
             */
            tap : function (target, callback, scope, options, offset, performTargetCheck) {
                this.checkTouchEventsSupport()

                target      = target || this.getCursorPagePosition()

                if (performTargetCheck !== false && callback) {
                    this.waitForTargetAndSyncMousePosition(
                        target, offset, this.tap, [ target, callback, scope, options, offset, false ]
                    );
                    return;
                }

                var context     = this.getNormalizedTopElementInfo(target, true, 'tap', offset);

                if (!context) {
                    callback && callback.call(scope || this);

                    return;
                }

                this.runPromiseAsync(
                    this.simulator.simulateTap(context, options),
                    'tap',
                    callback,
                    scope
                )
            },

            /**
             * This method double taps the passed target, which can be of several different types, see {@link Siesta.Test.ActionTarget}
             *
             * @param {Siesta.Test.ActionTarget} target Target for this action
             * @param {Function} callback (optional) A function to call after action.
             * @param {Object} scope (optional) The scope for the callback
             * @param {Object} options (optional) Any optionsthat will be used when simulating the event. For information about possible
             * config options, please see: <https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent>
             * @param {Array} offset (optional) An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "100%-2"] to click in the center horizontally and 2px from the bottom edge.
             */
            doubleTap : function (target, callback, scope, options, offset, performTargetCheck) {
                this.checkTouchEventsSupport()

                target      = target || this.getCursorPagePosition()

                if (performTargetCheck !== false && callback) {
                    this.waitForTargetAndSyncMousePosition(
                        target, offset, this.doubleTap, [ target, callback, scope, options, offset, false ]
                    );
                    return;
                }

                var context = this.getNormalizedTopElementInfo(target, true, 'doubleTap', offset);

                if (!context) {
                    callback && callback.call(scope || this);

                    return;
                }

                this.runPromiseAsync(
                    this.simulator.simulateDoubleTap(context, options),
                    'doubleTap',
                    callback,
                    scope
                )
            },

            // backward-compat with SenchaTouch class, which used to have all lower-cased method
            longpress : function () {
                return this.longPress.apply(this, arguments)
            },

            /**
             * This performs a long press on the passed target, which can be of several different types, see {@link Siesta.Test.ActionTarget}
             *
             * @param {Siesta.Test.ActionTarget} target Target for this action
             * @param {Function} callback (optional) A function to call after action.
             * @param {Object} scope (optional) The scope for the callback
             * @param {Object} options (optional) Any optionsthat will be used when simulating the event. For information about possible
             * config options, please see: <https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent>
             * @param {Array} offset (optional) An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "100%-2"] to click in the center horizontally and 2px from the bottom edge.
             */
            longPress : function (target, callback, scope, options, offset, performTargetCheck) {
                this.checkTouchEventsSupport()

                target      = target || this.getCursorPagePosition()

                if (performTargetCheck !== false && callback) {
                    this.waitForTargetAndSyncMousePosition(
                        target, offset, this.longPress, [ target, callback, scope, options, offset, false ]
                    );
                    return;
                }

                var context = this.getNormalizedTopElementInfo(target, true, 'longPress', offset);

                if (!context) {
                    callback && callback.call(scope || this);

                    return;
                }

                this.runPromiseAsync(
                    this.simulator.simulateLongPress(context, options),
                    'longPress',
                    callback,
                    scope
                )
            },

            /**
             * This method performs a pinch between the two specified points. It draws a line between the specified points and then moves 2 touches along that line,
             * so that the final distance between the touches becomes `scale * original distance`.
             *
             * This method can be called either in the full form with 2 different targets:
             *

        t.pinch("#grid > .col1", "#grid > .col2", 3, function () { ... })

             * or, in the short form, where the 2nd target argument is omitted:
             *

        t.pinch("#grid > .col1", 3, function () { ... })

             * In the latter form, `target2` is considered to be the same as `target1`.
             *
             * If `target1` and `target2` are the same, and no offsets are provided, offsets are set to the following values:
             *

        offset1     = [ '25%', '50%' ]
        offset2     = [ '75%', '50%' ]

             *
             *
             * @param {Siesta.Test.ActionTarget} target1 First point for pinch
             * @param {Siesta.Test.ActionTarget} target2 Second point for pinch. Can be omitted, in this case both points will belong to `target1`
             * @param {Number} scale The multiplier for a final distance between the points
             * @param {Function} callback A function to call after the pinch has completed
             * @param {Object} scope A scope for the `callback`
             * @param {Object} options (optional) Any optionsthat will be used when simulating the event. For information about possible
             * config options, please see: <https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent>
             * @param {Array} offset1 An X,Y offset relative to the target1. Example: [20, 20] for 20px or ["50%", "100%-2"]
             * for the point in the center horizontally and 2px from the bottom edge.
             * @param {Array} offset2 An X,Y offset relative to the target1. Example: [20, 20] for 20px or ["50%", "100%-2"]
             * for the point in the center horizontally and 2px from the bottom edge.
             */
            pinch : function (target1, target2, scale, callback, scope, options, offset1, offset2) {
                this.checkTouchEventsSupport()

                var me          = this;

                if (this.typeOf(target2) == 'Number') {
                    offset2     = offset1
                    offset1     = options
                    options     = scope
                    scope       = callback
                    callback    = scale
                    scale       = target2
                    target2     = target1
                }

                if (target2 == null) target2 = target1

                if (target1 == target2 && !offset1 && !offset2) {
                    offset1     = [ '25%', '50%' ]
                    offset2     = [ '75%', '50%' ]
                }

                var context1    = this.getNormalizedTopElementInfo(target1, true, 'pinch: target1', offset1);
                var context2    = this.getNormalizedTopElementInfo(target2, true, 'pinch: target2', offset2);

                if (!context1 || !context2) {
                    var R  = Siesta.Resource('Siesta.Test.Browser');

                    this.waitFor({
                        method          : function () {
                            var el1     = me.normalizeElement(target1, true)
                            var el2     = me.normalizeElement(target2, true)

                            return el1 && me.elementIsTop(el1, true, offset) && el2 && me.elementIsTop(el2, true, offset)
                        },
                        callback        : function () {
                            me.pinch(target1, target2, scope, callback, scope, options, offset1, offset2)
                        },
                        assertionName   : 'waitForTarget',
                        description     : ' ' + R.get('target') + ' "' + target1 + '" and "' + target2 + '" ' + R.get('toAppear')
                    });

                    return
                }

                this.runPromiseAsync(
                    this.simulator.simulatePinch(context1, context2, options),
                    'pinch',
                    callback,
                    scope
                )
            },

            /**
             * This method will simulate a drag and drop operation between either two points or two DOM elements.
             *
             * @param {Siesta.Test.ActionTarget} source {@link Siesta.Test.ActionTarget} value for the drag starting point
             * @param {Siesta.Test.ActionTarget} target {@link Siesta.Test.ActionTarget} value for the drag end point
             * @param {Function} callback A function to call after the drag operation is completed.
             * @param {Object} scope (optional) the scope for the callback
             * @param {Object} options (optional) Any optionsthat will be used when simulating the event. For information about possible
             * config options, please see: <https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent>
             * @param {Boolean} dragOnly true to skip the mouseup and not finish the drop operation.
             * @param {Array} sourceOffset (optional) An X,Y offset relative to the source. Example: [20, 20] for 20px or ["50%", "100%-2"] to click in the center horizontally and 2px from the bottom edge.
             * @param {Array} targetOffset (optional) An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "100%-2"] to click in the center horizontally and 2px from the bottom edge.
             */
            touchDragTo : function (source, target, callback, scope, options, dragOnly, sourceOffset, targetOffset) {
                var me          = this
                var context1    = this.getNormalizedTopElementInfo(source, true, 'touchDragTo: source', sourceOffset);
                var context2    = this.getNormalizedTopElementInfo(target, true, 'touchDragTo: target', targetOffset);

                if (!context1 || !context2) {
                    var R  = Siesta.Resource('Siesta.Test.Browser');

                    this.waitFor({
                        method          : function () {
                            var el1     = me.normalizeElement(source, true)
                            var el2     = me.normalizeElement(target, true)

                            return el1 && me.elementIsTop(el1, true, sourceOffset) && el2 && me.elementIsTop(el2, true, targetOffset)
                        },
                        callback        : function () {
                            me.touchDragTo(source, target, callback, scope, options, dragOnly, sourceOffset, targetOffset)
                        },
                        assertionName   : 'waitForTarget',
                        description     : ' ' + R.get('target') + ' "' + source + '" and "' + target + '" ' + R.get('toAppear')
                    });

                    return
                }

                this.runPromiseAsync(
                    this.simulator.simulateTouchDrag(context1.localXY, context2.localXY, options, dragOnly),
                    'touchDragTo',
                    callback,
                    scope
                )
            },

            /**
             * This method will simulate a drag and drop operation from a point (or DOM element) and move by a delta.
             *
             * @param {Siesta.Test.ActionTarget} source {@link Siesta.Test.ActionTarget} value as the drag starting point
             * @param {Array} delta The amount to drag from the source coordinate, expressed as [ x, y ]. E.g. [ 50, 10 ] will drag 50px to the right and 10px down.
             * @param {Function} callback A function to call after the drag operation is completed.
             * @param {Object} scope (optional) the scope for the callback
             * @param {Object} options (optional) Any optionsthat will be used when simulating the event. For information about possible
             * config options, please see: <https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent>
             * @param {Boolean} dragOnly true to skip the mouseup and not finish the drop operation.
             * @param {Array} offset (optional) An X,Y offset relative to the target. Example: [20, 20] for 20px or ["50%", "100%-2"] to click in the center horizontally and 2px from the bottom edge.
             */
            touchDragBy : function (source, delta, callback, scope, options, dragOnly, offset) {
                var me      = this;
                var context = this.getNormalizedTopElementInfo(source, true, 'touchDragBy', offset);

                if (!context) {
                    this.waitForTarget(source, function() {
                        this.touchDragBy(source, delta, callback, scope, options, dragOnly, offset)
                    }, this, null, offset)

                    return
                }

                var sourceXY        = context.globalXY;
                var targetXY        = [ sourceXY[ 0 ] + delta[ 0 ], sourceXY[ 1 ] + delta[ 1 ] ];

                this.runPromiseAsync(
                    this.simulator.simulateTouchDrag(sourceXY, targetXY, options, dragOnly),
                    'touchDragBy',
                    callback,
                    scope
                )
            },

            /**
             * This method will simulate a swipe operation between either two points or on a single DOM element.
             *
             * @param {Siesta.Test.ActionTarget} target Target for this action
             * @param {String} direction Either 'left', 'right', 'up' or 'down'
             * @param {Function} callback A function to call after the swing operation is completed
             * @param {Object} scope (optional) the scope for the callback
             * @param {Object} options (optional) Any options that will be used when simulating the event. For information about possible
             * config options, please see: <https://developer.mozilla.org/en-US/docs/DOM/event.initMouseEvent>
             */
            swipe : function (target, direction, callback, scope, options, performTargetCheck) {
                this.checkTouchEventsSupport()

                target      = target || this.getCursorPagePosition()

                if (performTargetCheck !== false && callback) {
                    this.waitForTargetAndSyncMousePosition(
                        target, null, this.swipe, [ target, direction, callback, scope, options, false ]
                    );
                    return;
                }

                var context = this.getNormalizedTopElementInfo(target, true, 'swipe');

                if (!context) {
                    callback && callback.call(scope || this);

                    return;
                }

                var Ext     = this.Ext()
                var R       = Siesta.Resource('Siesta.Test.SenchaTouch')

                var box     = Ext.fly(context.el).getBox(),
                    x       = box.x,
                    y       = box.y,
                    width   = box.width,
                    height  = box.height,
                    centerX = x + width / 2,
                    centerY = y + height / 2,
                    start,
                    end,
                    edgeCoef    = 0.1

                // Since this method accepts elements as target, we need to assure that we swipe at least about 150px
                // using Math.max below etc
                switch (direction) {
                    case 'u':
                    case 'up':
                        start       = [ centerX, y + height * (1 - edgeCoef) ];
                        end         = [ centerX, y + height * edgeCoef ];

                        end[ 1 ]    = Math.min(start[ 1 ] - 100, end[ 1 ]);
                    break;

                    case 'd':
                    case 'down':
                        start       = [ centerX, y + height * edgeCoef ];
                        end         = [ centerX, y + height * (1 - edgeCoef) ];

                        end[ 1 ]    = Math.max(start[ 1 ] + 100, end[ 1 ]);
                    break;

                    case 'r':
                    case 'right':
                        start       = [ x + width * edgeCoef, centerY ];
                        end         = [ x + width * (1 - edgeCoef), centerY ];

                        end[ 0 ]    = Math.max(start[ 0 ] + 100, end[ 0 ]);
                    break;

                    case 'l':
                    case 'left':
                        start       = [ x + width * (1 - edgeCoef), centerY ];
                        end         = [ x + width * edgeCoef, centerY ];

                        end[ 0 ]    = Math.min(start[ 0 ] - 100, end[ 0 ]);
                    break;

                    default:
                        throw R.get('invalidSwipeDir') + ': ' + direction;
                }

                this.touchDragTo(start, end, callback, scope, options);
            }
        }
    });