/*

Siesta 5.1.0
Copyright(c) 2009-2018 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"
//            }
        }
    }
});