/*

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

*/
// First fire a mouseover + mousemove on the target
// Based on Chrome's behavior
(function () {
    var postTapSequence                              = ['mouseover', /*'mouseenter'*/, 'mousemove', 'mousedown', 'mouseup', 'click'];
    var postLongPressSequence                        = ['mouseover', /*'mouseenter'*/, 'mousemove', 'contextmenu'];
    var postLongPressSequenceWithTouchStartPrevented = ['mousemove', 'contextmenu'];

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

        requires : [],

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

            currentTouchId : 1,

            activeTouches : Joose.I.Object,

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


        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)

                        // iOS Safari fires dblclick event
                        me.simulateEvent([],  'dblclick', 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, null)
                    }
                })
                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, true)
                    }
                })

                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, null)

                this.lastStartTouchWasOnNewTarget = !this.lastStartTouch || this.lastStartTouch.target !== context.el;
                this.lastStartTouchEvent          = event;
                // IE11 compat check
                this.lastStartTouch               = event.touches ? event.touches[0] : event;
                this.lastStartTouchTimeStamp      = Date.now();

                return event.pointerId != null ? event.pointerId : event.changedTouches[ 0 ].identifier
            },


            touchEnd : function (touchId, options) {
                touchId = touchId || Object.keys(this.activeTouches)[0];

                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.nodeIsOrphan(target)) {
                    touch.target = this.global.document.body
                }

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

            // Assumes an active touch exists
            touchMoveTo : function (toXY, options) {
                var touches = Object.keys(this.activeTouches);

                if (touches.length === 0) {
                    throw new Error('No active touch detected');
                }

                var touch       = this.activeTouches[ touches[0] ];

                return this.touchMove(touches[0], toXY[0], toXY[1], options);
            },

            // Assumes an active touch exists
            touchMoveBy : function (byXY, options) {
                var touches = Object.keys(this.activeTouches);

                if (touches.length === 0) {
                    throw new Error('No active touch detected');
                }

                var touch       = this.activeTouches[ touches[0] ];

                return this.touchMove(touches[0],this.currentPosition[0] + byXY[0], this.currentPosition[1] + byXY[1], options);
            },

            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 })
            },

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

                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)
                }

                var config = {
                    bubbles    : true,
                    cancelable : true,

                    changedTouches : this.createTouchList([ touch ]),
                    touches        : this.createTouchList(touches),
                    targetTouches  : this.createTouchList(targetTouches),

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

                try {
                    var event       = new global.TouchEvent(type, config)
                } catch(e) {
                    // Legacy branch
                    var event = new global.CustomEvent(type, {
                        bubbles    : true,
                        cancelable : true
                    })

                    Joose.O.extend(event, config);
                }

                target.dispatchEvent(event)

                return event
            },


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

                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);
                }

                return touchList;
            },


            createTouch : function (target, clientX, clientY, identifier) {
                var config = {
                    identifier : identifier || (this.currentTouchId++),
                    target     : target,

                    clientX : clientX,
                    clientY : clientY,

                    screenX : 0,
                    screenY : 0,

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

                if (this.global.Touch) {
                    return new this.global.Touch(config);
                }
                else {
                    return config;
                }
            },

            buildTouchEventNamesMap : function () {
                var supports                        = Siesta.Project.Browser.FeatureSupport().supports
                var supportsPointerEnterLeaveEvents = !this.test.bowser.safari;

                return {
                    pointer : {
                        start  : ['pointerover'].concat(supportsPointerEnterLeaveEvents ? 'pointerenter' : []).concat('pointerdown'),
                        move   : ['pointermove'],
                        end    : ['pointerup', 'pointerout'].concat(supportsPointerEnterLeaveEvents ? 'pointerleave' : []),
                        cancel : ['pointercancel']
                    },
                    touch   : {
                        start  : 'touchstart',
                        move   : 'touchmove',
                        end    : 'touchend',
                        cancel : 'touchcancel'
                    }
                };
            },


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

                var me = this;
                var target = me.test.normalizeElement(target)

                var clientX, clientY

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

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

                var activeTouches   = me.activeTouches
                var touch

                if (type === 'start') {
                    touch = me.createTouch(target, clientX, clientY)

                    activeTouches[ touch.identifier ] = touch
                } else if (type === 'move') {
                    touch = me.createTouch(target, options.clientX, options.clientY, simOptions.touchId)

                    // "*move" events should be fired only from the "movePointerTemplate" method
                    // which provides the "clientX/clientY" properties
                    touch = activeTouches[ simOptions.touchId ] = touch;
                } else if (type === 'end' || type === 'cancel') {
                    touch = activeTouches[ simOptions.touchId ]

                    target = touch.currentEl || touch.target

                    delete activeTouches[ simOptions.touchId ]
                }

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

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

                simOptions.touch = touch

                var eventMap       = this.getTouchEventNamesMap();
                var supports        = Siesta.Project.Browser.FeatureSupport().supports
                var pointerEvent;
                var touchEvent;

                if (supports.PointerEvents && !(type === 'end' && me.isLongPressing())) {
                    eventMap.pointer[type].forEach(function (event) {
                        pointerEvent = me.simulateEvent(target, event, Object.assign({ pointerType : 'touch' }, options), simOptions)
                    });
                }

                // IE11 compat check
                if (me.global.TouchEvent && (type !== 'end' || !this.isLongPressing())) {
                    touchEvent = me.simulateTouchEvent(target, eventMap.touch[type], options, simOptions);
                }

                // IE11 compat check
                if (touchEvent && type === 'start') {
                    me.lastPointerDownPrevented = me.isEventPrevented(touchEvent);
                }

                if (type === 'end') {
                    me.possiblySimulateMouseEventsForTouchEnd(target, touch, options);
                }

                // IE11 compat check
                return touchEvent || pointerEvent;
            },

            isLongPressing : function() {
                return Date.now() - this.lastStartTouchTimeStamp > this.getLongPressDelay();
            },

            // fires mouse events (done by the browser after native touch events)
            possiblySimulateMouseEventsForTouchEnd : function(target, touch, options) {
                var me = this;
                var didMove = me.lastStartTouch.clientX !== touch.clientX ||
                    me.lastStartTouch.clientY !== touch.clientY;

                if (!didMove) {
                    var sequence;
                    var startWasPrevented = this.isEventPrevented(this.lastStartTouchEvent);
                    // Don't fire mouse events if this is a move pointer move ("touchDrag") operation,
                    // Chrome actually fires mouse events also when you move a little, probably since touch
                    // by its nature is not exact typically.

                    if (this.isLongPressing()) {
                        if (startWasPrevented) {
                            sequence = postLongPressSequenceWithTouchStartPrevented;
                        }
                        else {
                            sequence = postLongPressSequence;
                        }
                    } else if (!startWasPrevented) {
                        sequence = this.lastStartTouchWasOnNewTarget ? postTapSequence : postTapSequence.filter(function(event) {
                            return event !== 'mouseover';
                        });
                    }

                    sequence && sequence.forEach(function (event) {
                        me.simulateEvent(target, event, options)
                    });
                }
            }
        }
    });
})();