/*

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

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

    requires        : [
        '$',
        'simulateEvent', 'isEventPrevented'
        /*'getSimulateEventsWith', 'getElementAtCursor'*/
    ],

    does : [
        Siesta.Util.Role.CanFormatStrings,
        Siesta.Test.Browser.Role.CanWorkWithKeyboard
    ],

    has : {
                                 // For "KeyboardEvent" only Firefox set values for deprecated keyCode/charCode, Chrome just sets them to 0
        keyboardEventName       : ("KeyboardEvent" in window && bowser.gecko) ? "KeyboardEvent" : ("KeyEvent" in window ? "KeyEvents" : null)
    },

    methods: {

        // TODO switch fully to KeyboardEvent https://developer.mozilla.org/en-US/docs/Web/API/KeyboardEvent/KeyboardEvent
        // private
        createKeyboardEvent: function (type, options, el) {
            var event;
            var doc = el.ownerDocument,
                global = this.global;

            options = $.extend({
                bubbles    : true,
                cancelable : true,
                view       : this.global,
                ctrlKey    : false,
                altKey     : false,
                shiftKey   : false,
                metaKey    : false,
                keyCode    : 0,
                charCode   : 0,
                key        : options.key || ''
            }, options);

            // use W3C standard when available and allowed by "simulateEventsWith" option
            if (doc.createEvent && this.getSimulateEventsWith() == 'dispatchEvent') {
                try {
                    if (this.keyboardEventName === 'KeyboardEvent') {
                        event = new KeyboardEvent(type, options);
                    }
                } catch (err) {
                    event = null;
                }

                if (!event) {
                    event = doc.createEvent("Events");
                    event.initEvent(type, options.bubbles, options.cancelable);

                    $.extend(event, {
                        view     : options.view,
                        ctrlKey  : options.ctrlKey,
                        altKey   : options.altKey,
                        shiftKey : options.shiftKey,
                        metaKey  : options.metaKey,
                        keyCode  : options.keyCode,
                        charCode : options.charCode,
                        key      : options.key || ''
                    });
                }
            } else if (doc.createEventObject) {
                event = doc.createEventObject();
                $.extend(event, options);
            }

            if (bowser.msie || bowser.opera) {
                event.keyCode = (options.charCode > 0) ? options.charCode : options.keyCode;
                event.charCode = undefined;
            }

            return event;
        },

        // private
        createTextEvent: function (type, options, el) {
            var doc         = el.ownerDocument;
            var event       = null;

            // only for Webkit / IE for now
            if (doc.createEvent) {
                try {
                    event = doc.createEvent('TextEvent');

                    if (event && event.initTextEvent) {
                        event.initTextEvent(
                            type,
                            true,
                            true,
                            this.global,
                            options.text,
                            // IE ONLY below here
                            0,
                            window.navigator.userLanguage || window.navigator.language
                        );
                        return event;
                    }
                }
                catch(e) {}
            }

            return null;
        },


        /*!
         * Based on:
         *
         * @license EmulateTab
         * Copyright (c) 2011, 2012 The Swedish Post and Telecom Authority (PTS)
         * Developed for PTS by Joel Purra <http://joelpurra.se/>
         * Released under the BSD license.
         *
         * A jQuery plugin to emulate tabbing between elements on a page.
         */
        findNextFocusable : function (el, offset) {
            var $el         = this.$(el)

            var $focusable  = this.$(":focus, :input, a[href], [tabindex], body", el.ownerDocument)
                .not(":disabled")
                .not(":hidden")
                .not("a[href]:empty")


            var escapeSelectorName  = function (str) {
                // Based on http://api.jquery.com/category/selectors/
                // Still untested
                return str.replace(/(!"#$%&'\(\)\*\+,\.\/:;<=>\?@\[\]^`\{\|\}~)/g, "\\\\$1");
            }

            var isRadio     = false
            var selector

            if (el.tagName === "INPUT" && el.type === "radio" && el.name !== "" ) {
                isRadio     = true
                selector    = "input[type=radio][name=" + escapeSelectorName(el.name) + "]"
            }

            var processed       = []

            for (var i = 0; i < $focusable.length; i++) {
                var currEl      = $focusable[ i ]

                // always include current element
                if (currEl != el && currEl.getAttribute('tabIndex') == -1 || isRadio && $(currEl).is(selector)) continue

                processed.push(currEl)
            }

            var body                = el.ownerDocument.body
            var currentTabIndex     = el.getAttribute('tabIndex')

            var getTabIndex         = function (dom) {
                if (dom == el && currentTabIndex == -1) return 0

                if (dom == body) return 0

                return dom.getAttribute('tabIndex') || 0
            }

            processed.sort(function (a, b) {
                var aIndex      = getTabIndex(a)
                var bIndex      = getTabIndex(b)

                return aIndex < bIndex ? -1 : (aIndex > bIndex ? 1 : (a == body ? 1 : (b == body ? -1 : 0)))
            });

            var currentIndex    = $(processed).index($el);

            if (currentIndex == -1) return null

            return processed[ (currentIndex + offset) % processed.length ]
        },


        emulateTab : function (el, offset) {
            var next        = this.findNextFocusable(el, offset || 1)

            if (next)
                this.test.focus(next)
            else
                el.blur()

            return next
        },


        simulateType : function (text, options, params) {
            if (text == null) throw 'Must supply a string to type';

            var me          = this

            var el          = params.el

            if (el.disabled) {
                return Promise.resolve()
            }

            // Store initial value of text fields, updated after ENTER key press in ´keyPress´ method
            if ('value' in el) {
                el.setAttribute('__lastValue', el.value);
            }

            // Extract normal chars, or special keys in brackets such as [TAB], [RIGHT] or [ENTER]
            var keys        = this.extractKeysAndSpecialKeys(text + '');

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

                interval        : this.actionDelay,
                // this is 0, since user agent `type` method also contains queue with "callbackDelay"
                // so we don't need to double that (which also breaks 624_rerun_hotkey)
                callbackDelay   : 0,

                observeTest     : this.test,

                processor       : function (data, index) {
                    // 1. In IE10, it seems activeElement cannot be trusted as it sometimes returns an empty object with no properties.
                    // Try to detect this case and simply use the original el
                    // 2. If user clicks around in the project during ongoing test, the activeElement will be reset to BODY
                    // If this happens, reuse the original el and hope all is well
                    var focusedEl   = me.activeElement(true, el, el)

                    me.simulateKeyPress(focusedEl, data.key, options)
                }
            })

            // the `el` should be already focused in the `type` method of the "user agent" code,
            // still allow to focus it, but using special "param.focus"
            if (params.focus) {
                // Manually focus event to be typed into first
                queue.addStep({
                    processor       : function () {
                        if (!me.nodeIsOrphan(el)) me.focus(el)
                    }
                })

                // focus the element one more time for IE - this seems to fix the weird sporadic failures in 042_keyevent_simulation3.t.js
                // failures are caused by the field "blur" immediately after 1st focus
                // no Ext "focus/blur" methods seems to be called, so it can be a browser behavior
                bowser.msie && queue.addStep({
                    processor       : function () {
                        if (!me.nodeIsOrphan(el)) me.focus(el)
                    }
                })
            }

            Joose.A.each(keys, function (key, index) {
                key             = key.length == 1 ? key : key.substring(1, key.length - 1)

                keys[ index ]   = key

                queue.addStep({ key : key })
            });

            if (!el.readOnly && keys.length) {
                var KeyCodes        = Siesta.Test.UserAgent.KeyCodes().keys;
                var firstKeyCode    = KeyCodes[ keys[ 0 ].toUpperCase() ]

                if (this.isReadableKey(firstKeyCode)) {
                    // Some browsers (IE/FF) do not overwrite selected text, do it manually
                    // but only if the key is readable (some letter etc)
                    // do not clear the selection in case of special symbol
                    var selText     = this.test.getSelectedText(el);

                    if (selText && 'value' in el && 'selectionStart' in el) {
                        var caretPos;

                        try {
                            caretPos = el.selectionStart;
                        } catch(e) {}

                        if (caretPos != null) {
                            // mimic replacing selected text
                            this.silentSetValue(el, el.value.substr(0, caretPos) + el.value.substr(caretPos + selText.length), 'value')

                            // Now set caret position to start of selection range
                            this.test.setCaretPosition(el, caretPos);
                        }
                    }
                }
            }

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


        simulateKeyPress: function (el, key, options) {
            var isMac           = bowser.mac;

            var KeyCodes        = Siesta.Test.UserAgent.KeyCodes().keys
            var keyNameMap      = Siesta.Test.UserAgent.KeyCodes().keyNameMap;
            var keyCode         = KeyCodes[ key.toUpperCase() ] || 0;
            var keyDownEl       = el = this.test.normalizeElement(el);

            options             = options || {};

            options.readableKey = key;

            // keypress should not be fired on Mac when CMD is pressed
            // nor on Windows when CTRL is pressed
            var suppressKeyPress = (isMac && options.metaKey) || (!isMac && options.ctrlKey);

            // Should not actually type anything when CTRL / CMD are pressed
            var isReadableKey   = this.isReadableKey(keyCode);
            var charCode        = isReadableKey && !suppressKeyPress ? key.charCodeAt(0) : 0

            options.key         = isReadableKey ? key : (keyNameMap[ keyCode ] || '');

            var me              = this,
                isTextInput     = me.isTextInput(el),
                isEditableNode  = me.isEditableNode(el),
                acceptsTextInput = isTextInput || isEditableNode;

            var textValueProp       = 'value' in el ? 'value' : 'innerHTML';
            var originalLength      = el[ textValueProp ].length;

            var keyDownEvent        = me.simulateEvent(el, 'keydown', Joose.O.extend({ charCode : 0, keyCode : keyCode }, options));
            var keyDownPrevented    = this.isEventPrevented(keyDownEvent)

            var isSelection         = acceptsTextInput && this.mimicTextSelection(keyDownEvent, el);

            if (!isSelection) {
                var keyPressPrevented = false;
                var supports          = Siesta.Project.Browser.FeatureSupport().supports

                // Need to reevaluate focused element here, it may have changed in a 'keydown' listener
                el                  = me.activeElement(true, el, el);

                // keypress should not be fired when CTRL or CMD are pressed
                if (!suppressKeyPress && !keyDownPrevented) {
                    var event         = me.simulateEvent(el, 'keypress', Joose.O.extend({ charCode : charCode, keyCode : isReadableKey ? 0 : keyCode }, options));
                    keyPressPrevented = this.isEventPrevented(event)

                    if (!keyPressPrevented && keyCode === KeyCodes.TAB) {
                        el          = this.emulateTab(el, options.shiftKey ? -1 : 1) || el;
                    }
                }

                if (!keyDownPrevented && acceptsTextInput && keyCode != KeyCodes.TAB) {

                    if (isReadableKey && !suppressKeyPress && !keyPressPrevented) {
                        var innerHTML

                        // IE10 tries to be 'helpful' by inserting an empty space, clean it
                        // IE11 inserts <br> after call to the .focus() method of the element
                        if (isEditableNode && bowser.msie) {
                            innerHTML               = el.innerHTML

                            if (innerHTML.indexOf('&nbsp;') === 0) {
                                el.innerHTML        = innerHTML.substring(6)
                                originalLength      = el.innerHTML.length
                            } else
                                if (innerHTML.indexOf('<br>') === 0) {
                                    el.innerHTML    = innerHTML.substring(4);
                                    originalLength  = el.innerHTML.length
                                }
                        }

                        // IE won't do execCommand with insertText
                        if (isEditableNode && !bowser.msie) {
                            innerHTML           = el.innerHTML

                            if (innerHTML.charCodeAt(innerHTML.length - 1) === 8203) {
                                el.innerHTML    = innerHTML.substring(0, innerHTML.length - 1);
                            }
                            el.ownerDocument.execCommand('insertText', false, options.readableKey);
                        } else {
                             //TODO should check first if textInput event is supported
                            me.simulateEvent(el, bowser.msie ? 'textinput' : 'textInput', { text: options.readableKey });
                        }

                        // will fire 'input' event
                        me.mimicCharacterInsertion(el, key, options, originalLength);
                    } else {
                        me.mimicCaretMovement(el, keyCode);
                    }

                    // Manually delete one char off the end if backspace simulation is not supported by the browser
                    if (
                        (keyCode === KeyCodes.BACKSPACE || keyCode === KeyCodes.DELETE)
                        && !supports.canSimulateBackspace && el[ textValueProp ].length > 0
                    ) {
                        this.mimicCharacterDeletion(el, keyCode, options);
                    }

                    if (textValueProp === 'value' && keyCode === KeyCodes.ENTER && !keyPressPrevented) {
                        if (isTextInput) this.maybeMimicChangeEvent(keyDownEl);

                        if (!supports.enterSubmitsForm) {
                            this.mimicFormSubmit(el);
                        }
                    }
                }
            }

            this.mimicClickOnEnter(el, keyCode);

            me.simulateEvent(el, 'keyup', $.extend({ charCode : 0, keyCode : keyCode }, options));

            return Promise.resolve()
        },


        mimicCharacterInsertion : function (el, readableKey, options, originalLength) {
            var textValueProp   = 'value' in el ? 'value' : 'innerHTML';

            var maxLength       = el.getAttribute('maxlength') || Infinity
            var isTextInput     = this.isTextInput(el);
            var supports        = Siesta.Project.Browser.FeatureSupport().supports;

            if (maxLength != null) maxLength    = Number(maxLength)

            // If the entered char had no impact on the textfield - manually put it there
            if (
                !el.readOnly && (isTextInput || bowser.msie)
                && !supports.canSimulateKeyCharacters
                && originalLength === el[ textValueProp ].length && originalLength < maxLength
            ) {
                var val         = el[ textValueProp ];
                var caretPos    = this.test.getCaretPosition(el);

                // Fallback to appending text to end of string if caret position cannot be determined
                if (caretPos == undefined) {
                    caretPos    = val.length;
                }

                // Inject char at caret position
                this.silentSetValue(
                    el,
                    val.substr(0, caretPos) + readableKey + val.substr(caretPos),
                    textValueProp
                )

                // Restore caret position
                this.test.setCaretPosition(el, caretPos + 1);

                this.simulateEvent(el, 'input', options);
            }
        },


        // this method will change the property `propertyName` of the `el` to a `newValue`
        // if `propertyName` will be "value" it will try to avoid "touching" the actual "value"
        // property, since that may trigger side effects
        // if user has defined own "value" property on the element (React did that, crazy)
        silentSetValue : function (el, newValue, propetyName) {
            var Object      = this.global.Object

            if (Object.getOwnPropertyDescriptor && propetyName == 'value') {
                var desc    = Object.getOwnPropertyDescriptor(el.constructor.prototype, propetyName)

                desc.set.call(el, newValue)
            } else
                el[ propetyName ] = newValue;
        },


        mimicTextSelection : function(keyDownEvent, el) {
            var isMac       = bowser.mac;
            var KC          = Siesta.Test.UserAgent.KeyCodes().keys;

            var retVal      = false;

            // CTRL-A or CMD-A in text input should select all
            var ctrlKey     = (!isMac && keyDownEvent.ctrlKey) || (keyDownEvent.metaKey && isMac);

            switch (keyDownEvent.keyCode) {
                // Select all
                case KC["A"]:
                    if (ctrlKey) {
                        this.test.selectText(el);
                        retVal = true;
                    }
                    break;

                case KC["LEFT"]:
                case KC["HOME"]:
                    if (keyDownEvent.shiftKey) {
                        this.test.selectText(el, 0, this.test.getCaretPosition(el));
                        retVal = true;
                    }
                    break;

                case KC["RIGHT"]:
                case KC["END"]:
                    if (keyDownEvent.shiftKey) {
                        this.test.selectText(el, this.test.getCaretPosition(el));
                        retVal = true;
                    }
                    break;
            }

            return retVal;
        },


        mimicClickOnEnter : function (el, keyCode) {
            // somehow "node.nodeName" is empty sometimes in IE10
            var nodeName        = el.nodeName && el.nodeName.toLowerCase()
            var supports        = Siesta.Project.Browser.FeatureSupport().supports
            var KeyCodes        = Siesta.Test.UserAgent.KeyCodes().keys

            if ((nodeName == 'a' || nodeName == 'button') && keyCode === KeyCodes.ENTER && !supports.enterOnAnchorTriggersClick) {
                // this "click" should not update the current cursor position its merely for activating "click" listeners
                this.simulateEvent(el, 'click', { doNotUpdateCurrentPosition : true });
            }
        },


        mimicCaretMovement : function(el, keyCode) {
            // somehow "node.nodeName" is empty sometimes in IE10
            var nodeName        = el.nodeName && el.nodeName.toLowerCase()

            if ((nodeName == 'input' || nodeName == 'textarea')) {
                var KeyCodes        = Siesta.Test.UserAgent.KeyCodes().keys;

                switch (keyCode) {
                    case KeyCodes.HOME:
                        this.test.setCaretPosition(el, 0);
                        break;

                    case KeyCodes.LEFT:
                        var selText  = this.test.getSelectedText(el);

                        if (selText) {
                            var caretPos = this.test.getCaretPosition(el);

                            this.test.selectText(el, caretPos, caretPos);
                        } else {
                            this.test.moveCaretPosition(el, -1);
                        }
                        break;

                    case KeyCodes.RIGHT:
                        var selText  = this.test.getSelectedText(el);

                        if (selText) {
                            var caretPos = this.test.getCaretPosition(el);

                            this.test.selectText(el, caretPos + selText.length, caretPos + selText.length);
                        } else {
                            this.test.moveCaretPosition(el, 1);
                        }
                        break;

                    case KeyCodes.END:
                        this.test.setCaretPosition(el, el.value.length);
                        break;
                }
            }
        },


        mimicFormSubmit : function (el) {
            var form        = this.$(el).closest('form');

            if (form.length) {
                // Use jQuery's :submit instead of [type=submit] since <button>Foo</button> could have button.type=submit, but this is not queryable
                var submitButton = form.find(':submit')[ 0 ];
                var hasOneInput  = form.find('input').length === 1;

                if (submitButton) {
                    submitButton.click();
                }
                else if (hasOneInput) {
                    var submitPrevented = this.isEventPrevented(this.simulateEvent(form[ 0 ], 'submit', {}));

                    if (!submitPrevented) form[ 0 ].submit();
                }
            }
        },


        mimicCharacterDeletion : function (el, keyCode, options) {
            var isTextInput     = this.isTextInput(el);

            if (!el.readOnly) {
                // IE won't do execCommand with insertText
                if (isTextInput || bowser.msie) {
                    var textValueProp       = 'value' in el ? 'value' : 'innerHTML';
                    var text                = el[ textValueProp ];

                    var selText             = this.test.getSelectedText(el) || '';
                    var caretPosition       = this.test.getCaretPosition(el);

                    var inputChanged        = false

                    if (caretPosition != null && selText) {
                        this.silentSetValue(
                            el,
                            text.substring(0, caretPosition) + text.substring(caretPosition + selText.length),
                            textValueProp
                        )

                        inputChanged        = true
                    } else {
                        var KeyCodes        = Siesta.Test.UserAgent.KeyCodes().keys;

                        // fall back to last char index if caret position could not be determined
                        caretPosition       = caretPosition == null ? text.length : caretPosition;

                        if (keyCode === KeyCodes.BACKSPACE) {
                            if (caretPosition > 0) {
                                inputChanged = true

                                this.silentSetValue(
                                    el,
                                    text.substring(0, caretPosition - 1) + text.substring(caretPosition),
                                    textValueProp
                                )

                                caretPosition       = caretPosition - 1;
                            }
                        } else {
                            if (caretPosition < text.length) inputChanged = true

                            // DELETE key
                            this.silentSetValue(
                                el,
                                (caretPosition > 0 ? text.substring(0, caretPosition + 1) : '') + text.substring(caretPosition + 1),
                                textValueProp
                            )
                        }
                    }

                    // Caret position is moved to end when setting text value, restore it manually
                    this.test.setCaretPosition(el, caretPosition);

                    inputChanged && this.simulateEvent(el, 'input', options);
                } else {
                    el.ownerDocument.execCommand('delete');
                }
            }
        },


        maybeMimicChangeEvent : function (el) {
            if (el.getAttribute('__lastValue') !== el.value) {
                this.simulateEvent(el, 'change');

                el.setAttribute('__lastValue', el.value);
            }
        }
    }
});