odoo.define('web.KeyboardNavigationMixin', function (require) {
    "use strict";
    var BrowserDetection = require('web.BrowserDetection');

    /**
     * list of the key that should not be used as accesskeys. Either because we want to reserve them for a specific behavior in Odoo or
     * because they will not work in certain browser/OS
     */
    var knownUnusableAccessKeys = [' ',
        'A', // reserved for Odoo Edit
        'C', // reserved for Odoo Create
        'H', // reserved for Odoo Home
        'J', // reserved for Odoo Discard
        'K', // reserved for Odoo Kanban view
        'L', // reserved for Odoo List view
        'N', // reserved for Odoo pager Next
        'P', // reserved for Odoo pager Previous
        'S', // reserved for Odoo Save
        'Q', // reserved for Odoo Search
        'E', // chrome does not support 'E' access key --> go to address bar to search google
        'F', // chrome does not support 'F' access key --> go to menu
        'D', // chrome does not support 'D' access key --> go to address bar
        '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' // reserved for Odoo menus
    ];

    var KeyboardNavigationMixin = {
        events: {
            'keydown': '_onKeyDown',
            'keyup': '_onKeyUp',
        },

        init: function () {
            this._areAccessKeyVisible = false;
            this.BrowserDetection = new BrowserDetection();
        },

        //--------------------------------------------------------------------------
        // Private
        //--------------------------------------------------------------------------

        /**
         * @private
         */
        _addAccessKeyOverlays: function () {
            var accesskeyElements = $(document).find('[accesskey]').filter(':visible');
            _.each(accesskeyElements, function (elem) {
                var overlay = $(_.str.sprintf("<div class='o_web_accesskey_overlay'>%s</div>", $(elem).attr('accesskey').toUpperCase()));

                var $overlayParent;
                if (elem.tagName.toUpperCase() === "INPUT") {
                    // special case for the search input that has an access key
                    // defined. We cannot set the overlay on the input itself,
                    // only on its parent.
                    $overlayParent = $(elem).parent();
                } else {
                    $overlayParent = $(elem);
                }

                if ($overlayParent.css('position') !== 'absolute') {
                    $overlayParent.css('position', 'relative');
                }
                overlay.appendTo($overlayParent);
            });
        },
        /**
         * @private
         * @return {jQuery[]}
         */
        _getAllUsedAccessKeys: function () {
            var usedAccessKeys = knownUnusableAccessKeys.slice();
            this.$el.find('[accesskey]').each(function (_, elem) {
                usedAccessKeys.push(elem.accessKey.toUpperCase());
            });
            return usedAccessKeys;
        },
        /**
         * hides the overlay that shows the access keys.
         *
         * @private
         * @param $parent {jQueryElemen} the parent of the DOM element to which shorcuts overlay have been added
         * @return {undefined|jQuery}
         */
        _hideAccessKeyOverlay: function () {
            this._areAccessKeyVisible = false;
            var overlays = this.$el.find('.o_web_accesskey_overlay');
            if (overlays.length) {
                return overlays.remove();
            }
        },
        /**
         * @private
         */
        _setAccessKeyOnTopNavigation: function () {
            this.$el.find('.o_menu_sections>li>a').each(function (number, item) {
                item.accessKey = number + 1;
            });
        },

        //--------------------------------------------------------------------------
        // Handlers
        //--------------------------------------------------------------------------

        /**
         * Assign access keys to all buttons inside $el and sets an overlay to show the access key
         * The access keys will be assigned using first the name of the button, letter by letter until we find one available,
         * after that we will assign any available letters.
         * Not all letters should be used as access keys, some of the should be reserved for standard odoo behavior or browser behavior
         *
         * @private
         * @param keyDownEvent {jQueryKeyboardEvent} the keyboard event triggered
         * return {undefined|false}
         */
        _onKeyDown: function (keyDownEvent) {
            if ($('body.o_ui_blocked').length &&
            (keyDownEvent.altKey || keyDownEvent.key === 'Alt') &&
            !keyDownEvent.ctrlKey) {
                if (keyDownEvent.preventDefault) keyDownEvent.preventDefault(); else keyDownEvent.returnValue = false;
                if (keyDownEvent.stopPropagation) keyDownEvent.stopPropagation();
                if (keyDownEvent.cancelBubble) keyDownEvent.cancelBubble = true;
                return false;
            }
            if (!this._areAccessKeyVisible &&
                (keyDownEvent.altKey || keyDownEvent.key === 'Alt') &&
                !keyDownEvent.ctrlKey) {

                this._areAccessKeyVisible = true;

                this._setAccessKeyOnTopNavigation();

                var usedAccessKey = this._getAllUsedAccessKeys();

                var buttonsWithoutAccessKey = this.$el.find('button.btn:visible')
                    .not('[accesskey]')
                    .not('[disabled]')
                    .not('[tabindex="-1"]');
                _.each(buttonsWithoutAccessKey, function (elem) {
                    var buttonString = [elem.innerText, elem.title, "ABCDEFGHIJKLMNOPQRSTUVWXYZ"].join('');
                    for (var letterIndex = 0; letterIndex < buttonString.length; letterIndex++) {
                        var candidateAccessKey = buttonString[letterIndex].toUpperCase();
                        if (candidateAccessKey >= 'A' && candidateAccessKey <= 'Z' &&
                            !_.includes(usedAccessKey, candidateAccessKey)) {
                            elem.accessKey = candidateAccessKey;
                            usedAccessKey.push(candidateAccessKey);
                            break;
                        }
                    }
                });

                var elementsWithoutAriaKeyshortcut = this.$el.find('[accesskey]').not('[aria-keyshortcuts]');
                _.each(elementsWithoutAriaKeyshortcut, function (elem) {
                    elem.setAttribute('aria-keyshortcuts', 'Alt+Shift+' + elem.accessKey);
                });
                this._addAccessKeyOverlays();
            }
            // on mac, there are a number of keys that are only accessible though the usage of
            // the ALT key (like the @ sign in most keyboards)
            // for them we do not facilitate the access keys, so they will need to be activated classically
            // though Control + Alt + key (case sensitive), see https://developer.mozilla.org/en-US/docs/Web/HTML/Global_attributes/accesskey
            if (this.BrowserDetection.isOsMac())
                return;

            if (keyDownEvent.altKey && !keyDownEvent.ctrlKey && keyDownEvent.key.length === 1) { // we don't want to catch the Alt key down, only the characters A to Z and number keys
                var elementWithAccessKey = [];
                if (keyDownEvent.keyCode >= 65 && keyDownEvent.keyCode <= 90 || keyDownEvent.keyCode >= 97 && keyDownEvent.keyCode <= 122) {
                    // 65 = A, 90 = Z, 97 = a, 122 = z
                    elementWithAccessKey = document.querySelectorAll('[accesskey="' + String.fromCharCode(keyDownEvent.keyCode).toLowerCase() +
                        '"], [accesskey="' + String.fromCharCode(keyDownEvent.keyCode).toUpperCase() + '"]');
                    if (elementWithAccessKey.length) {
                        if (this.BrowserDetection.isOsMac() ||
                            !this.BrowserDetection.isBrowserChrome()) { // on windows and linux, chrome does not prevent the default of the accesskeys
                            elementWithAccessKey[0].focus();
                            elementWithAccessKey[0].click();
                            if (keyDownEvent.preventDefault) keyDownEvent.preventDefault(); else keyDownEvent.returnValue = false;
                            if (keyDownEvent.stopPropagation) keyDownEvent.stopPropagation();
                            if (keyDownEvent.cancelBubble) keyDownEvent.cancelBubble = true;
                            return false;
                        }
                    }
                }
                else {
                    // identify if the user has tapped on the number keys above the text keys.
                    // this is not trivial because alt is a modifier and will not input the actual number in most keyboard layouts
                    var numberKey;
                    if (keyDownEvent.originalEvent.code && keyDownEvent.originalEvent.code.indexOf('Digit') === 0) {
                        //chrome & FF have the key Digit set correctly for the numbers
                        numberKey = keyDownEvent.originalEvent.code[keyDownEvent.originalEvent.code.length - 1];
                    } else if (keyDownEvent.originalEvent.key &&
                        keyDownEvent.originalEvent.key.length === 1 &&
                        keyDownEvent.originalEvent.key >= '0' &&
                        keyDownEvent.originalEvent.key <= '9') {
                        //edge does not use 'code' on the original event, but the 'key' is set correctly
                        numberKey = keyDownEvent.originalEvent.key;
                    } else if (keyDownEvent.keyCode >= 48 && keyDownEvent.keyCode <= 57) {
                        //fallback on keyCode if both code and key are either not set or not digits
                        numberKey = keyDownEvent.keyCode - 48;
                    }

                    if (numberKey >= '0' && numberKey <= '9') {
                        elementWithAccessKey = document.querySelectorAll('[accesskey="' + numberKey + '"]');
                        if (elementWithAccessKey.length) {
                            elementWithAccessKey[0].click();
                            if (keyDownEvent.preventDefault) keyDownEvent.preventDefault(); else keyDownEvent.returnValue = false;
                            if (keyDownEvent.stopPropagation) keyDownEvent.stopPropagation();
                            if (keyDownEvent.cancelBubble) keyDownEvent.cancelBubble = true;
                            return false;
                        }
                    }
                }
            }
        },
        /**
         * hides the shortcut overlays when keyup event is triggered on the ALT key
         *
         * @private
         * @param keyUpEvent {jQueryKeyboardEvent} the keyboard event triggered
         * @return {undefined|false}
         */
        _onKeyUp: function (keyUpEvent) {
            if ((keyUpEvent.altKey || keyUpEvent.key === 'Alt') && !keyUpEvent.ctrlKey) {
                this._hideAccessKeyOverlay();
                if (keyUpEvent.preventDefault) keyUpEvent.preventDefault(); else keyUpEvent.returnValue = false;
                if (keyUpEvent.stopPropagation) keyUpEvent.stopPropagation();
                if (keyUpEvent.cancelBubble) keyUpEvent.cancelBubble = true;
                return false;
            }
        },
    };

    return KeyboardNavigationMixin;

});

