/**
 * This class is used internally to inject synthetic DOM events.
 * @class ST.event.Injector
 * @private
 */
ST.event.Injector = ST.define({
    /**
     * @cfg {Boolean} translate 
     * `false` to disable event translation.  If `false` events that are not supported by
     * the browser's event APIs will simply be skipped.
     */
    translate: true,
 
    constructor: function(config) {
        ST.apply(this, config);
    },
 
    /**
     * Injects a synthetic event
     * @param {Object/String} event The event type or descriptor
     * @param {ST.Element/HTMLElement} target
     * @param {ST.Element/HTMLElement} relatedTarget
     */
    injectEvent: function(event, target, relatedTarget) {
        if (typeof event === 'string') {
            event = { type: event };
        }
        var me = this,
            Event = ST.event.Event,
            supports = ST.supports,
            supportsPointer = supports.PointerEvents,
            supportsMSPointer = supports.MSPointerEvents,
            supportsTouch = supports.TouchEvents,
            doDefaultAction = true,
            type = event.type,
            isMouseEvent = Event.mouseEvents[type],
            pointerType = event.pointerType,
            isWebKitDesktop = ST.isWebKit && ST.os.is.Desktop,
            translatedEvent, isMouse, keypress;
 
        target = target || event.target;
        relatedTarget = relatedTarget || event.relatedTarget;
 
        if (target && target.dom) {
            target = target.dom;
        }
 
        if (relatedTarget && relatedTarget.dom) {
            relatedTarget = relatedTarget.dom;
        }
 
        if (Event.keyEvents[type]) {
            doDefaultAction = me.fireKeyEvent(event, target) && doDefaultAction;
            if (type === 'keydown') {
                // Event recordings do not include keypress events, so as to simplify 
                // the output for the user.  To match real-world browser behavior we 
                // simulate a keypress after all keydown events during playback. 
                keypress = ST.chain(event);
                keypress.type = 'keypress';
                doDefaultAction = me.fireKeyEvent(keypress, target) && doDefaultAction;
            }
        } else if (event.translate === false || me.translate === false) {
            if (isMouseEvent || Event.clickEvents[type]) {
                doDefaultAction = me.fireMouseEvent(event, target, relatedTarget) && doDefaultAction;
            } else if (Event.pointerEvents[type]) {
                // The recorder always translates MS-prefixed pointer events to regular pointer 
                // events, so we must always translate back, event if "translate" is false 
                if (supportsMSPointer && !supportsPointer) {
                    event = me.translateEvent(event, Event.pointerToMS);
                }
                doDefaultAction = me.firePointerEvent(event, target, relatedTarget) && doDefaultAction;
            } else if (Event.touchEvents[type]) {
                doDefaultAction = me.fireTouchEvent(event, target) && doDefaultAction;
            }
        } else if (isMouseEvent || Event.clickEvents[type]) {
            if (supportsPointer || supportsMSPointer) {
                translatedEvent = me.translateEvent(event, Event.mouseToPointer);
 
                if (translatedEvent && !supportsPointer) {
                    translatedEvent = me.translateEvent(translatedEvent, Event.pointerToMS);
                }
 
                if (translatedEvent) {
                    doDefaultAction = me.firePointerEvent(translatedEvent, target, relatedTarget) && doDefaultAction;
                }
            }
 
            if (supportsTouch && !isWebKitDesktop) {
                translatedEvent = me.translateEvent(event, Event.mouseToTouch);
 
                if (translatedEvent) {
                    doDefaultAction = me.fireTouchEvent(translatedEvent, target) && doDefaultAction;
                }
            } else {
                doDefaultAction = me.fireMouseEvent(event, target, relatedTarget) && doDefaultAction;
            }
        } else if (Event.pointerEvents[type]) {
            // Note we only need to check for pointerEvents because recording never contains 
            // MS-prefixed pointer events - those are translated into regular pointer events 
            // by the recorder 
            if (supportsPointer) {
                doDefaultAction = me.firePointerEvent(event, target, relatedTarget) && doDefaultAction;
            } else if (supportsMSPointer) {
                translatedEvent = me.translateEvent(event, Event.pointerToMS);
 
                if (translatedEvent) {
                    doDefaultAction = me.firePointerEvent(translatedEvent, target, relatedTarget) && doDefaultAction;
                }
            }
 
            if (supportsTouch) {
                isMouse = !pointerType || (pointerType === 'mouse');
 
                if (isMouse && isWebKitDesktop) {
                    translatedEvent = me.translateEvent(event, Event.pointerToMouse);
                    if (translatedEvent) {
                        doDefaultAction = me.fireMouseEvent(translatedEvent, target, relatedTarget) && doDefaultAction;
                    }
                } else {
                    translatedEvent = me.translateEvent(event, Event.pointerToTouch);
                    if (translatedEvent) {
                        doDefaultAction = me.fireTouchEvent(translatedEvent, target) && doDefaultAction;
                    }
                }
            } else {
                translatedEvent = me.translateEvent(event, Event.pointerToMouse);
 
                if (translatedEvent) {
                    doDefaultAction = me.fireMouseEvent(translatedEvent, target, relatedTarget) && doDefaultAction;
                }
            }
        } else if (Event.touchEvents[type]) {
            if (supportsPointer || supportsMSPointer) {
                translatedEvent = me.translateEvent(event, Event.touchToPointer, { pointerType: 'touch' });
 
                if (!supportsPointer) {
                    translatedEvent = me.translateEvent(translatedEvent, Event.pointerToMS);
                }
 
                if (translatedEvent) {
                    doDefaultAction = me.firePointerEvent(translatedEvent, target, relatedTarget) && doDefaultAction;
                }
 
                translatedEvent = me.translateEvent(event, Event.touchToMouse);
 
                if (translatedEvent) {
                    doDefaultAction = me.fireMouseEvent(translatedEvent, target, relatedTarget) && doDefaultAction;
                }
            } else if (supportsTouch) {
                doDefaultAction = me.fireTouchEvent(event, target) && doDefaultAction;
            } else {
                translatedEvent = me.translateEvent(event, Event.touchToMouse);
 
                if (translatedEvent) {
                    doDefaultAction = me.fireMouseEvent(translatedEvent, target, relatedTarget) && doDefaultAction;
                }
            }
        }
 
        if (doDefaultAction) {
            me.defaultAction(event, target);
        }
    },
 
    translateEvent: function(event, translationMap, props) {
        var type = translationMap[event.type],
            translatedEvent = null;
 
        if (type) {
            translatedEvent = ST.chain(event);
            translatedEvent.type = type;
            if (props) {
                ST.apply(translatedEvent, props);
            }
        }
 
        return translatedEvent;
    },
 
    fireMouseEvent: function (event, target, relatedTarget) {
        var me = this,
            Event = ST.event.Event,
            type = event.type,
            bubbles = true,
            cancelable = true,
            coordinates = me.getCoordinates(event, target),
            pageX = coordinates.pageX,
            pageY = coordinates.pageY,
            clientX = coordinates.clientX,
            clientY = coordinates.clientY,
            ctrlKey = !!event.ctrlKey,
            shiftKey = !!event.shiftKey,
            altKey = !!event.altKey,
            metaKey = !!event.metaKey,
            detail = event.detail || (Event.detailEvents[type] ? 1 : 0),
            button = event.button,
            buttons = event.buttons,
            doc = target.ownerDocument || document,
            view = doc.defaultView || doc.parentWindow,
            movementX = event.movementX,
            movementY = event.movementY,
            lastMouseMove = me.lastMouseMove,
            dispatchTarget = me.getDispatchTarget(target, clientX, clientY),
            ret = true,
            e, docEl, body;
 
        relatedTarget = relatedTarget || null;
 
        if (button == null && buttons != null) {
            button = Event.buttonsToButton[buttons];
        }
 
        if (buttons == null && button != null) {
            buttons = Event.buttonToButtons[button];
        }
 
        if (movementX == null) {
            movementX = lastMouseMove ? (clientX - lastMouseMove.clientX) : 0
        }
 
        if (movementY == null) {
            movementY = lastMouseMove ? (clientY - lastMouseMove.clientY) : 0
        }
 
        button = button || 0;
        buttons = buttons || (type === 'mousedown' ? 1 : 0);
 
        if (me.player) {
            me.player.onPointChanged(pageX, pageY);
        }
 
        if (ST.supports.EventConstructors) {
            e = new MouseEvent(type, {
                bubbles: bubbles,
                cancelable: cancelable,
                view: view,
                detail: detail,
                screenX: pageX,
                screenY: pageY,
                clientX: clientX,
                clientY: clientY,
                ctrlKey: ctrlKey,
                shiftKey: shiftKey,
                altKey: altKey,
                metaKey: metaKey,
                button: button,
                buttons: buttons,
                relatedTarget: relatedTarget,
                movementX: movementX,
                movementY: movementY
            });
 
            e.eventId = event.id;
 
            // Safari has a bug where the buttons property is not set even though we passed 
            // it to the constructor. 
            if (e.buttons == null) {
                e.buttons = buttons;
            }
 
            // Safari does not yet support movementX/Y via MouseEvent constructor 
            if (e.movementX == null) {
                e.movementX = movementX;
            }
            if (e.movementY == null) {
                e.movementY = movementY;
            }
 
            ret = dispatchTarget.dispatchEvent(e);
        } else if (ST.isIE9m && doc.createEventObject) {
            // IE8 and IE9. 
            // Although IE9 supports the newer event model (addEventListener and createEvent) 
            // events created using createEvent/initMouseEvent in IE9 do not have the correct 
            // pageX and pageY (always 0).  Because of issues such as this Ext JS has historically 
            // used the older event model in IE9 for listening to events.  Listeners must 
            // use the same event system as that which was used to fire the event, for 
            // example, an event fired using fireEvent cannot be listened to using 
            // addEventListener.  This means the event player must follow the framework 
            // and use the old event model in IE9. 
            // TODO: could we fire an additional event using the new event model for non-ext apps? 
            e = doc.createEventObject();
            docEl = doc.documentElement;
            body = doc.body;
            pageX = pageX + (docEl && docEl.clientLeft || 0) + (body && body.clientLeft || 0);
            pageY = pageY + (docEl && docEl.clientTop || 0) + (body && body.clientLeft || 0);
            ST.apply(e, {
                bubbles: bubbles,
                cancelable: cancelable,
                screenX: pageX,
                screenY: pageY,
                clientX: clientX,
                clientY: clientY,
                // in the old IE event model "button" is a bit mask like the new standard 
                // "buttons" property 
                button: buttons,
                shiftKey: shiftKey,
                ctrlKey: ctrlKey,
                altKey: altKey,
                eventId: event.id
                // TODO: toElement and fromElement? 
            });
 
            dispatchTarget.fireEvent('on' + type, e);
            ret = e.returnValue !== false;
        } else if (doc.createEvent) {
            e = doc.createEvent("MouseEvents");
 
            e.initMouseEvent(
                type,
                bubbles,
                cancelable,
                view,
                detail,
                pageX,
                pageY,
                clientX,
                clientY,
                ctrlKey,
                altKey,
                shiftKey,
                metaKey,
                button,
                relatedTarget
            );
 
            e.eventId = event.id;
 
            ret = dispatchTarget.dispatchEvent(e);
        } else {
            console.warn('Cannot play "' + type + '".  Browser does not support this type of event.');
        }
 
        if (type === 'mousemove') {
            me.lastMouseMove = e;
        }
 
        return ret;
    },
 
    firePointerEvent: function (event, target, relatedTarget) {
        if (!ST.PointerEvents && !ST.supports.MSPointerEvents) {
            console.warn('Cannot play "' + event.type + '".  Browser does not support this type of event.');
        }
 
        var me = this,
            Event = ST.event.Event,
            type = event.type,
            bubbles = true,
            cancelable = true,
            coordinates = me.getCoordinates(event, target),
            pageX = coordinates.pageX,
            pageY = coordinates.pageY,
            clientX = coordinates.clientX,
            clientY = coordinates.clientY,
            ctrlKey = !!event.ctrlKey,
            shiftKey = !!event.shiftKey,
            altKey = !!event.altKey,
            metaKey = !!event.metaKey,
            detail = event.detail || (Event.detailEvents[type] ? 1 : 0),
            button = event.button,
            buttons = event.buttons,
            doc = target.ownerDocument || document,
            view = doc.defaultView || doc.parentWindow,
            movementX = event.movementX,
            movementY = event.movementY,
            lastMouseMove = me.lastMouseMove,
            pointerId = event.pointerId || 1,
            width = event.width || 1,
            height = event.height || 1,
            pressure = event.pressure,
            tiltX = event.tiltX || 0,
            tiltY = event.tiltY || 0,
            pointerType = event.pointerType || 'mouse',
            isPrimary = event.isPrimary,
            dispatchTarget = me.getDispatchTarget(target, clientX, clientY),
            ret = true,
            e;
 
        relatedTarget = relatedTarget || null;
 
        if (button == null && buttons != null) {
            button = Event.buttonsToButton[buttons];
        }
 
        if (buttons == null && button != null) {
            buttons = Event.buttonToButtons[button];
        }
 
        if (pressure == null) {
            pressure = buttons ? 0.5 : 0;
        }
 
        if (movementX == null) {
            movementX = lastMouseMove ? (clientX - lastMouseMove.clientX) : 0
        }
 
        if (movementY == null) {
            movementY = lastMouseMove ? (clientY - lastMouseMove.clientY) : 0
        }
 
        if (isPrimary == null) {
            isPrimary = true;
        }
 
        button = button || 0;
        buttons = buttons || (type === 'pointerdown' ? 1 : 0);
 
        if (!ST.supports.PointerEvents) {
            // IE10 - translate pointerType to number 
            pointerType = Event.msPointerTypes[pointerType];
        }
 
        if (me.player) {
            me.player.onPointChanged(pageX, pageY);
        }
 
        if (ST.supports.EventConstructors) {
            e = new PointerEvent(type, {
                bubbles: bubbles,
                cancelable: cancelable,
                view: view,
                detail: detail,
                screenX: pageX,
                screenY: pageY,
                clientX: clientX,
                clientY: clientY,
                ctrlKey: ctrlKey,
                shiftKey: shiftKey,
                altKey: altKey,
                metaKey: metaKey,
                button: button,
                buttons: buttons,
                relatedTarget: relatedTarget,
                movementX: movementX,
                movementY: movementY,
                pointerId: pointerId,
                width: width,
                height: height,
                pressure: pressure,
                tiltX: tiltX,
                tiltY: tiltY,
                pointerType: pointerType,
                isPrimary: isPrimary
            });
 
            e.eventId = event.id;
 
            ret = dispatchTarget.dispatchEvent(e);
        } else if (doc.createEvent) {
            // If the browser supports pointer events but does not support the PointerEvents 
            // constructor we have to fake it by constructing a mouse event a setting 
            // the extra pointer event properties on the event object after initialization. 
            e = doc.createEvent("MouseEvents");
 
            e.initMouseEvent(
                type,
                bubbles,
                cancelable,
                view,
                detail,
                pageX,
                pageY,
                clientX,
                clientY,
                ctrlKey,
                altKey,
                shiftKey,
                metaKey,
                button,
                relatedTarget
            );
 
            e.pointerId = pointerId;
            e.width = width;
            e.height = height;
            e.pressure = pressure;
            e.tiltX = tiltX;
            e.tiltY = tiltY;
            e.pointerType = pointerType;
            e.isPrimary = isPrimary;
            e.eventId = event.id;
 
            ret = dispatchTarget.dispatchEvent(e);
        }
 
        return ret;
    },
 
    fireTouchEvent: function(event, target) {
        if (!ST.supports.TouchEvents) {
            console.warn('Cannot play "' + event.type + '".  Browser does not support this type of event.');
        }
 
        // TODO: ORION-42 - support multi-touch recording 
 
        var me = this,
            type = event.type,
            coordinates = me.getCoordinates(event, target),
            pageX = coordinates.pageX,
            pageY = coordinates.pageY,
            clientX = coordinates.clientX,
            clientY = coordinates.clientY,
            ctrlKey = !!event.ctrlKey,
            shiftKey = !!event.shiftKey,
            altKey = !!event.altKey,
            metaKey = !!event.metaKey,
            dispatchTarget = me.getDispatchTarget(target, clientX, clientY),
            touches = me.createTouchList([{
                pageX: pageX,
                pageY: pageY,
                clientX: clientX,
                clientY: clientY,
                identifier: event.pointerId || 1,
                target: dispatchTarget
            }]),
            targetTouches = touches,
            changedTouches = touches,
        // It doesn't appear to be possible to set touches, changedTouches targetTouches 
        // on a "real" TouchEvent, initTouchEvent seems to ignore those parameters: 
        // Directly assigning to e.touches after creating a TouchEvent doesn't work 
        // either so the best we can do is just make a CustomEvent and fake it. 
            e = new CustomEvent(type, {
                bubbles: true,
                cancelable: true,
                detail: 0
            });
 
        if (me.player) {
            me.player.onPointChanged(pageX, pageY);
        }
 
        ST.apply(e, {
            ctrlKey: ctrlKey,
            altKey: altKey,
            shiftKey: shiftKey,
            metaKey: metaKey,
            touches: touches,
            targetTouches: targetTouches,
            changedTouches: changedTouches,
            eventId: event.id
        });
 
        return dispatchTarget.dispatchEvent(e);
    },
 
    createTouchList: function(touchList, target) {
        var doc = document,
            ln = touchList.length,
            touches = [],
            touchCfg, i;
 
        for (= 0; i < ln; i++) {
            touchCfg = touchList[i];
            touches.push(doc.createTouch(
                doc.defaultView || doc.parentWindow,
                touchCfg.target || target,
                // use 1 as the default ID, so that tests that are only concerned with a single 
                // touch event don't need to worry about providing an ID 
                touchCfg.identifier || 1,
                touchCfg.pageX,
                touchCfg.pageY,
                touchCfg.screenX || touchCfg.pageX, // use pageX/Y as the default for screenXY 
                touchCfg.screenY || touchCfg.pageY
            ));
        }
 
        return doc.createTouchList.apply(doc, touches);
    },
 
    fireKeyEvent: function(event, target)  {
        var type = event.type,
            bubbles = true,
            cancelable = true,
            key = event.key || '',
            code = event.code || '',
            charCode = event.charCode || 0,
            keyCode = event.keyCode || 0,
            ctrlKey = !!event.ctrlKey,
            shiftKey = !!event.shiftKey,
            altKey = !!event.altKey,
            metaKey = !!event.metaKey,
            doc = target.ownerDocument || document,
            view = doc.defaultView || doc.parentWindow,
            isKeypress = (type === 'keypress'),
            KeyMap = ST.KeyMap,
            activeElement = document.activeElement,
            ret = true,
            e, error;
 
        if (!key) {
            if (keyCode) {
                key = KeyMap.lookupKey(keyCode);
            } else if (!isKeypress) {
                error = 'Cannot play ' + type + ' event without either a "key" or "keyCode" property.'
            }
 
            if (!key && !charCode && isKeypress) {
                error = 'Cannot play keypress event without either a "key" or "charCode" property.'
            }
 
            if (error) {
                this.error(error);
                return;
            }
        }
 
        if (key in KeyMap.reverseShiftKeys) {
            shiftKey = true;
        }
 
        if (isKeypress) {
            if (!charCode) {
                if (key.length === 1) {
                    charCode = key.charCodeAt(0);
                } else {
                    charCode = KeyMap.lookupKeyCode(key);
                }
            }
 
            if (!keyCode) {
                keyCode = charCode;
            }
        } else if (!keyCode) {
            keyCode = KeyMap.lookupKeyCode(key);
        }
 
        if (target !== activeElement) {
            target.focus();
        }
 
        if (ST.supports.KeyboardEventConstructor) {
            e = new KeyboardEvent(type, {
                bubbles: bubbles,
                cancelable: cancelable,
                view: view,
                detail: 0,
                key: key,
                code: code,
                //location: ? 
                ctrlKey: ctrlKey,
                shiftKey: shiftKey,
                altKey: altKey,
                metaKey: metaKey,
                //repeat: ? 
                //isComposing: ? 
                charCode: charCode,
                keyCode: keyCode
                //which: ? 
            });
 
            ret = target.dispatchEvent(e);
        } else if (ST.isIE9m && doc.createEventObject) {
            e = doc.createEventObject();
            ST.apply(e, {
                bubbles: bubbles,
                cancelable: bubbles,
                key: key,
                code: code,
                ctrlKey: ctrlKey,
                shiftKey: shiftKey,
                altKey: altKey,
                metaKey: metaKey,
                charCode: charCode,
                keyCode: keyCode
            });
            target.fireEvent('on' + type, e);
            ret = e.returnValue !== false;
        } else {
            e = doc.createEvent("Events");
            e.initEvent(type, bubbles, cancelable);
            ST.apply(e, {
                key: key,
                code: code,
                ctrlKey: ctrlKey,
                shiftKey: shiftKey,
                altKey: altKey,
                metaKey: metaKey,
                charCode: charCode,
                keyCode: keyCode
            });
            ret = target.dispatchEvent(e);
        }
 
        return ret;
    },
 
    fireInputEvent: function(target) {
        var e;
        if (!ST.isIE9m) {
            e = document.createEvent("Events");
            e.initEvent('input', true, false);
            return target.dispatchEvent(e);
        }
    },
 
    /**
     * To more accurately simulate real browser behavior, use the element at clientXY
     * as the dispatch target and rely on bubbling for the event to reach the target.
     * @param {HTMLElement} target 
     * @param {Number} clientX 
     * @param {Number} clientY 
     */
    getDispatchTarget: function(target, clientX, clientY) {
        var dispatchTarget = document.elementFromPoint(clientX, clientY);
 
        if (!dispatchTarget || !ST.fly(target).contains(dispatchTarget)) {
            // If element at clientXY is not a descendant of the target (or the target itself) 
            // then ignore it and just dispatch to the target. 
            dispatchTarget = target;
        }
 
        return dispatchTarget;
    },
 
    /**
     * Given an event descriptor with target-relative x and y coordinates, calculates
     * "page" and "client" coordinates required for event dispatching.
     * If the event descriptor is missing either x or y coordinate, a default coordinate
     * at the center of the target element will be calculated.
     * @param {Object} event The event descriptor
     * @param {HTMLElement} target The event target
     * @return {Object} The coordinates
     * @return {Number} return.pageX
     * @return {Number} return.pageY
     * @return {Number} return.clientX
     * @return {Number} return.clientY
     */
    getCoordinates: function(event, target) {
        var origin = ST.fly(target).getXY(),
            pageScroll = ST.fly(document).getScroll(),
            x = event.x,
            y = event.y,
            pageX, pageY;
 
        if (== null) {
            x = target.offsetWidth / 2;
        }
 
        if (== null) {
            y = target.offsetHeight / 2;
        }
 
        pageX = origin[0] + x;
        pageY = origin[1] + y;
 
        return {
            pageX: pageX,
            pageY: pageY,
            clientX: pageX - pageScroll.x,
            clientY: pageY - pageScroll.y
        };
    },
 
    /**
     * When firing synthetic events browsers to not always fire the default action of those
     * events (for example a synthetic mousedown does not trigger focus).
     * This method is invoked after every event to mimic the default action for applicable events.
     * @param {Object} event The event descriptor
     * @param {HTMLElement} target 
     * @private
     */
    defaultAction: function(event, target) {
        var type = event.type,
            activeElement = document.activeElement,
            key, keyCode, contentEditable;
 
        if (type === 'mousedown' || type === 'pointerdown') {
            // TODO: skip this if the recording contains a focus event? 
            target.focus();
            if (activeElement && (activeElement !== target)) {
                /**
                 * target was not a focusable element, throw focus back to the body
                 *
                 * Cannot execute blur on activeElement as it won't have relatedTarget
                 * which Ext JS checks for
                 */
                document.body.focus();
            }
        } else if (type === 'click') {
            if (target.tagName === 'A' && location.href !== target.href) {
                // IE8 and old firefox do not navigate on synthetic click 
                //location.href = target.href; 
            }
        } else if (type === 'keydown' && ST.fly(target).isUserEditable()) {
            // simulate typing. If key changes value fire an input event 
            key = event.key;
            keyCode = event.keyCode;
 
            if (!key && keyCode) {
                key = ST.KeyMap.lookupKey(keyCode);
            }
 
            // TODO: handle selection/cursor placement 
            if (key && key.length === 1) {
                contentEditable = target.contentEditable;
                if (contentEditable === '' || contentEditable === 'true') {
                    target.innerHTML += key;
                } else {
                    target.value += key;
                }
                this.fireInputEvent(target);
            }
        }
    },
 
    error: function(message) {
        var player = this.player;
 
        if (player) {
            player.fireEvent('error', player, message);
        } else {
            throw message;
        }
    }
});