    /*

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

    */
    Role('Siesta.Test.Simulate.Touch', {

        requires        : [
        ],

        has: {
            touchEventNamesMap  : {
                lazy        : 'this.buildTouchEventNamesMap'
            },

            currentTouchId  : 1,

            activeTouches   : Joose.I.Object,

            longPressDelay      : {
                init        : 1500,
                is          : 'rw'
            },

            forceTouchEvents    : bowser.chrome
        },

        methods: {

            simulateTap : function (context, options) {

                var queue       = new Siesta.Util.Queue({
                    deferer         : this.test.originalSetTimeout,
                    deferClearer    : this.test.originalClearTimeout,

                    interval        : 30,

                    observeTest     : this.test
                })

                var me          = this;
                var id

                queue.addStep({
                    processor : function () {
                        id      = me.touchStart(null, null, options, context)
                    }
                })
                queue.addStep({
                    processor : function () {
                        me.touchEnd(id, options)
                    }
                })

                return new Promise(function (resolve, reject) {
                    queue.run(resolve)
                })
            },

            simulateDoubleTap : function (context, options) {

                var queue       = new Siesta.Util.Queue({
                    deferer         : this.test.originalSetTimeout,
                    deferClearer    : this.test.originalClearTimeout,

                    interval        : 30,

                    observeTest     : this.test
                })

                var me      = this;
                var id

                queue.addStep({
                    processor : function () {
                        id      = me.touchStart(null, null, options, context)
                    }
                })
                queue.addStep({
                    processor : function () {
                        me.touchEnd(id, options)
                    }
                })
                queue.addStep({
                    processor : function () {
                        id      = me.touchStart(null, null, options, context)
                    }
                })
                queue.addStep({
                    processor : function () {
                        me.touchEnd(id, options)
                    }
                })

                return new Promise(function (resolve, reject) {
                    queue.run(resolve)
                })
            },

            simulateLongPress : function (context, options) {

                var queue       = new Siesta.Util.Queue({
                    deferer         : this.test.originalSetTimeout,
                    deferClearer    : this.test.originalClearTimeout,

                    interval        : 30,

                    observeTest     : this.test
                })

                var me      = this;
                var id

                queue.addStep({
                    processor : function () {
                        id      = me.touchStart(null, null, options, context)
                    }
                })
                queue.addDelayStep(this.getLongPressDelay())
                queue.addStep({
                    processor : function () {
                        me.touchEnd(id, options)
                    }
                })

                return new Promise(function (resolve, reject) {
                    queue.run(resolve)
                })
            },

            simulatePinch : function (context1, context2, options) {

                var queue       = new Siesta.Util.Queue({
                    deferer         : this.test.originalSetTimeout,
                    deferClearer    : this.test.originalClearTimeout,

                    interval        : 30,

                    observeTest     : this.test
                })

                var id1, id2

                var dx          = context1.localXY[ 0 ] - context2.localXY[ 0 ]
                var dy          = context1.localXY[ 1 ] - context2.localXY[ 1 ]

                var distance    = Math.sqrt(dx * dx + dy * dy)

                if (distance < 1) distance = 1

                var scaled      = distance * scale
                var delta       = (scaled - distance) / 2

                var angle       = Math.atan(dy / dx)

                var x1          = Math.round(context1.localXY[ 0 ] - delta * Math.cos(angle))
                var y1          = Math.round(context1.localXY[ 1 ] - delta * Math.sin(angle))

                var x2          = Math.round(context2.localXY[ 0 ] + delta * Math.cos(angle))
                var y2          = Math.round(context2.localXY[ 1 ] + delta * Math.sin(angle))

                var options2    = Joose.O.extend({}, options)

                queue.addStep({
                    processor : function () {
                        id1     = me.touchStart(null, null, options, context1)
                        id2     = me.touchStart(null, null, options2, context2)
                    }
                })
                queue.addAsyncStep({
                    processor : function (data) {
                        var move1Done   = false
                        var move2Done   = false

                        me.touchMove(id1, x1, y1, function () {
                            move1Done       = true

                            if (move1Done && move2Done) data.next()
                        }, null, options)

                        me.touchMove(id2, x2, y2, function () {
                            move2Done       = true

                            if (move1Done && move2Done) data.next()
                        }, null, options2)
                    }
                })
                queue.addStep({
                    processor : function () {
                        me.touchEnd(id1, options)
                        me.touchEnd(id2, options2)
                    }
                })

                return new Promise(function (resolve, reject) {
                    queue.run(resolve)
                })
            },

            simulateTouchDrag : function (sourceXY, targetXY, options, dragOnly) {
                var me          = this
                options         = options || {};

                // For drag operations we should always use the top level document.elementFromPoint
                var source      = me.elementFromPoint(sourceXY[ 0 ], sourceXY[ 1 ], true);
                var target      = me.elementFromPoint(targetXY[ 0 ], targetXY[ 1 ], true);

                var queue       = new Siesta.Util.Queue({
                    deferer         : this.test.originalSetTimeout,
                    deferClearer    : this.test.originalClearTimeout,

                    interval        : me.dragDelay,
                    callbackDelay   : me.afterActionDelay,

                    observeTest     : this.test
                });

                var id

                queue.addStep({
                    processor : function () {
                        id      = me.touchStart(sourceXY, null, options)
                    }
                })
                queue.addAsyncStep({
                    processor : function (data) {
                        me.touchMove(id, targetXY[ 0 ], targetXY[ 1 ], options).then(data.next)
                    }
                })
                queue.addStep({
                    processor : function () {
                        // if `dragOnly` flag is set, do not finalize the touch, instead, pass the touch id
                        // to the user in the callback (see below)
                        if (!dragOnly) me.touchEnd(id, options)
                    }
                })

                return new Promise(function (resolve, reject) {
                    queue.run(function () {
                        // if `dragOnly` flag is set pass the touch id as promise result
                        if (dragOnly)
                            resolve(id)
                        else
                            resolve()
                    })
                })
            },

            touchStart : function (target, offset, options, context) {
                if (!context) context = this.test.getNormalizedTopElementInfo(target, true, 'touchStart', offset)

                options         = Joose.O.extend({
                    clientX     : context.localXY[ 0 ],
                    clientY     : context.localXY[ 1 ]
                }, options || {})

                var event       = this.simulateTouchEventGeneric(context.el, 'start', options)

                return event.pointerId || event.changedTouches[ 0 ].identifier
            },

            touchEnd : function (touchId, options) {
                var touch       = this.activeTouches[ touchId ]

                if (!touch) throw "Can't find active touch: " + touchId

                options         = Joose.O.extend({
                    clientX     : touch.clientX,
                    clientY     : touch.clientY
                }, options || {})

                var target      = touch.target

                if (!this.test.isInDom(target)) {
                    touch.target = this.global.document.body
                }

                this.simulateTouchEventGeneric(touch.currentEl || touch.target, 'end', options, { touchId : touchId })
            },

            touchMove : function (touchId, toX, toY, options) {
                var touch       = this.activeTouches[ touchId ]

                if (!touch) throw "Can't find active touch: " + touchId

                var me          = this
                var overEls     = []

                return this.movePointerTemplate({
                    xy              : [ touch.clientX, touch.clientY ],
                    xy2             : [ toX, toY ],
                    options         : options || {},

                    overEls         : overEls,
                    interval        : me.dragDelay,
                    callbackDelay   : me.afterActionDelay,
                    pathBatchSize   : me.pathBatchSize,

                    onVoidOverEls   : function () {
                        return overEls = []
                    },

                    onPointerEnter  : function (el, options) {
                    },

                    onPointerLeave  : function (el, options) {
                    },

                    onPointerOver   : function (el, options) {
                    },

                    onPointerOut    : function (el, options) {
                    },

                    onPointerMove   : function (el, options) {
                        touch.clientX       = options.clientX
                        touch.clientY       = options.clientY

                        touch.pageX         = me.viewportXtoPageX(options.clientX)
                        touch.pageY         = me.viewportYtoPageY(options.clientY)

                        touch.currentEl     = el

                        me.simulateTouchEventGeneric(el, 'move', options, { touchId : touchId })
                    }
                })
            },

            // never used yet, should be called when touchMove goes out of the document
            touchCancel : function (touchId, options) {
                var touch       = this.activeTouches[ touchId ]

                if (!touch) throw "Can't find active touch: " + touchId

                this.simulateTouchEventGeneric(touch.currentEl || touch.target, 'cancel', options, { touchId : touchId })
            },

            simulatePointerEventModern : function (target, type, options, simOptions) {
                target          = this.test.normalizeElement(target)

                if (!target) return false

                var supports    = Siesta.Project.Browser.FeatureSupport().supports

                options         = options || {}

                if (/pointerdown$/i.test(type) && (!("clientX" in options) || !("clientY" in options))) {
                    var center  = this.test.findCenter(target);

                    options     = Joose.O.extend({
                        clientX     : center[ 0 ],
                        clientY     : center[ 1 ]
                    }, options)
                }

                var doc         = this.global.document

                var event       = new PointerEvent(type, {
                    bubbles         : true,
                    cancelable      : true,

                    view            : this.global,
                    detail          : options.detail,

                    screenX         : options.screenX,
                    screenY         : options.screenY,

                    clientX         : options.clientX,
                    clientY         : options.clientY,

                    ctrlKey         : options.ctrlKey || false,
                    altKey          : options.altKey || false,
                    shiftKey        : options.shiftKey || false,
                    metaKey         : options.metaKey || false,

                    button          : options.button,
                    relatedTarget   : options.relatedTarget || doc.documentElement,

                    pointerId       : simOptions.touchId || this.currentTouchId++,

                    // pointerType
                    // NOTE: this has to be set to "mouse" (IE11) or 4 (IE10, 11) because otherwise
                    // ExtJS5 blocks the event
                    // need to investigate what happens in SenchaTouch
                    pointerType     : 4 //mouse
                })

                target.dispatchEvent(event)

                return event
            },

            simulatePointerEvent : function (target, type, options, simOptions) {
                var supports    = Siesta.Project.Browser.FeatureSupport().supports

                if (supports.PointerEvents && this.bowser.chrome) return this.simulatePointerEventModern(target, type, options, simOptions)

                target          = this.test.normalizeElement(target)

                if (!target) return false

                options         = options || {}

                var doc         = this.global.document,
                    event       = doc.createEvent(
                        supports.PointerEvents ? 'PointerEvent' : supports.MSPointerEvents ? 'MSPointerEvent' : 'MouseEvents'
                    )

                if (/pointerdown$/i.test(type) && (!("clientX" in options) || !("clientY" in options))) {
                    var center  = this.test.findCenter(target);

                    options     = Joose.O.extend({
                        clientX     : center[ 0 ],
                        clientY     : center[ 1 ]
                    }, options)
                }

                event[ (supports.MSPointerEvents || supports.PointerEvents) ? 'initPointerEvent' : 'initMouseEvent' ](
                    type, true, true, this.global, options.detail,
                    options.screenX, options.screenY, options.clientX, options.clientY,
                    options.ctrlKey || false, options.altKey || false, options.shiftKey || false, options.metaKey || false,
                    options.button, options.relatedTarget || doc.documentElement,
                    // the following extra args are used in the "initPointerEvent"
                    // offsetX, offsetY
                    null, null,
                    // width, height
                    null, null,
                    // pressure, rotation
                    null, null,
                    // tiltX, tiltY
                    null, null,
                    // pointerId
                    simOptions.touchId || this.currentTouchId++,
                    // pointerType
                    // NOTE: this has to be set to "mouse" (IE11) or 4 (IE10, 11) because otherwise
                    // ExtJS5 blocks the event
                    // need to investigate what happens in SenchaTouch
                    4,//'mouse',
                    // timestamp, isPrimary
                    null, null
                );

                if (!(supports.MSPointerEvents || supports.PointerEvents)) {
                    event.pointerId = simOptions.touchId || this.currentTouchId++
                }

                target.dispatchEvent(event)

                return event
            },

            simulateTouchEvent : function (target, type, options, simOptions) {
                options         = options || {}
                var global      = this.global
                var doc         = global.document

                var event       = new global.CustomEvent(type, {
                    bubbles     : true,
                    cancelable  : true
                })

                var target      = this.test.normalizeElement(target)

                var clientX, clientY

                if (("clientX" in options) && ("clientY" in options)) {
                    clientX     = options.clientX
                    clientY     = options.clientY
                } else {
                    var center  = this.test.findCenter(target);

                    clientX     = center[ 0 ]
                    clientY     = center[ 1 ]
                }

                var activeTouches   = this.activeTouches
                var touch           = simOptions.touch
                var touches         = []
                var targetTouches   = []

                for (var id in activeTouches) {
                    var currentTouch    = activeTouches[ id ]

                    touches.push(currentTouch)
                    if (currentTouch.target == target) targetTouches.push(currentTouch)
                }

                Joose.O.extend(event, {
                    target          : target,

                    changedTouches  : this.createTouchList([ touch ]),

                    touches         : this.createTouchList(touches),
                    targetTouches   : this.createTouchList(targetTouches),

                    altKey          : options.altKey,
                    metaKey         : options.metaKey,
                    ctrlKey         : options.ctrlKey,
                    shiftKey        : options.shiftKey
                });

                target.dispatchEvent(event)

                return event
            },

            createTouchList : function  (touchList) {
                var doc         = this.global.document

                // a branch for browsers supporting "createTouch/createTouchList"
                if (doc.createTouch) {
                    var touches = [];

                    for (var i = 0; i < touchList.length; i++) {
                        var touchCfg    = touchList[ i ];

                        touches.push(doc.createTouch(
                            doc.defaultView || doc.parentWindow,
                            touchCfg.target,
                            touchCfg.identifier || this.currentTouchId++,
                            touchCfg.pageX,
                            touchCfg.pageY,
                            touchCfg.screenX || touchCfg.pageX,
                            touchCfg.screenY || touchCfg.pageY,
                            touchCfg.clientX,
                            touchCfg.clientY
                        ))
                    }

                    return doc.createTouchList.apply(doc, touches);
                } else
                    return touchList
            },

            createTouch: function (target, clientX, clientY) {
                return {
                    identifier  : this.currentTouchId++,
                    target      : target,

                    clientX     : clientX,
                    clientY     : clientY,

                    screenX     : 0,
                    screenY     : 0,

                    // TODO should take scrolling into account
                    pageX       : clientX,
                    pageY       : clientY
                }
            },

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

                return supports.PointerEvents && !this.forceTouchEvents ?
                    {
                        start   : 'pointerdown',
                        move    : 'pointermove',
                        end     : 'pointerup',
                        cancel  : 'pointercancel'
                    }
                    : supports.MSPointerEvents ?
                        {
                            start   : 'MSPointerDown',
                            move    : 'MSPointerMove',
                            end     : 'MSPointerUp',
                            cancel  : 'MSPointerCancel'
                        }
                        : /*supports.TouchEvents ?*/
                        {
                            start   : 'touchstart',
                            move    : 'touchmove',
                            end     : 'touchend',
                            cancel  : 'touchcancel'
                        }
    //                :
    //                // todo: fire mouseevents?
    //                (function () { throw "Touch events not supported" })()
            },

            simulateTouchEventGeneric : function (target, type, options, simOptions) {
                simOptions      = simOptions || {}

                var target      = this.test.normalizeElement(target)

                var clientX, clientY

                if (("clientX" in options) && ("clientY" in options)) {
                    clientX     = options.clientX
                    clientY     = options.clientY
                } else {
                    var center  = this.test.findCenter(target);

                    clientX     = center[ 0 ]
                    clientY     = center[ 1 ]
                }

                var activeTouches   = this.activeTouches
                var touch

                if (type === 'end' || type === 'cancel') {
                    touch       = activeTouches[ simOptions.touchId ]

                    target      = touch.currentEl || touch.target

                    delete activeTouches[ simOptions.touchId ]
                } else if (type == 'start') {
                    touch       = this.createTouch(target, clientX, clientY)

                    activeTouches[ touch.identifier ] = touch

                } else if (type == 'move') {
                    touch           = activeTouches[ simOptions.touchId ]

                    // "*move" events should be fired only from the "movePointerTemplate" method
                    // which provides the "clientX/clientY" properties
                    touch.clientX   = options.clientX
                    touch.clientY   = options.clientY
                }

                if (!touch) throw "Can't find active touch" + (simOptions.touchId ? ': ' + simOptions.touchId : '')

                if (!simOptions.touchId) simOptions.touchId = touch.identifier

                simOptions.touch    = touch

                var eventType       = this.getTouchEventNamesMap()[ type ]
                var supports        = Siesta.Project.Browser.FeatureSupport().supports

                if ((supports.PointerEvents || supports.MSPointerEvents) && !this.forceTouchEvents) {
                    return this.simulatePointerEvent(target, eventType, options, simOptions)
                } else /*if (supports.TouchEvents)*/ {
                    return this.simulateTouchEvent(target, eventType, options, simOptions);
                }
    //            } else {
    //                // TODO fallback to mouse events?
    //                throw "Can't simulate any type of touch events"
    //            }
            }
        }
    });