/**
 * A menu object. This is the container to which you may add {@link Ext.menu.Item menu items}.
 *
 * Menus may contain either {@link Ext.menu.Item menu items}, or general
 * {@link Ext.Component Components}. Menus may also contain docked items because it
 * extends {@link Ext.Panel}.
 *
 * By default, Menus are absolutely positioned, floated Components. By configuring a
 * Menu with `{@link #cfg-floated}: false`, a Menu may be used as a child of a 
 * {@link Ext.Container Container}.
 *
 *     @example
 *     var mainPanel = Ext.create('Ext.Panel', {
 *         fullscreen: true,
 *
 *         items: {
 *             xtype: 'menu',
 *             floated: false,
 *             docked: 'left',
 *             items: [{
 *                 text: 'regular item 1'
 *             },{
 *                 text: 'regular item 2'
 *             },{
 *                 text: 'regular item 3'
 *             }]
 *         }
 *     });
 *
 * @since 6.5.0
 */
Ext.define('Ext.menu.Menu', {
    extend: 'Ext.Panel',
    xtype: 'menu',
 
    requires: [
        'Ext.menu.Item',
        'Ext.menu.Manager',
        'Ext.layout.VBox'
    ],
 
    /**
     * @property {Boolean} isMenu
     * `true` in this class to identify an object as an instantiated Menu, or subclass thereof.
     */
    isMenu: true,
 
    config: {
        /**
         * @cfg {String} align
         */
        align: 'tl-bl?', // TODO: Override in RTL
 
        /**
         * @cfg {Boolean} indented
         * By default menu items reserve space at their start for an icon.  Set indented
         * to `false` to remove this space.  This behavior can be overridden at the level
         * of an individual menu item using the item's {@link Ext.menu.Item#indented} config.
         * Items that are not {@link Ext.menu.Item Menu Items} can provide an `indented`
         * property in their initial config object to control their indentation behavior.
         *
         * When set to `false` no vertical `separator` will be shown.
         */
        indented: true,
 
        /**
         * @cfg {Boolean} [separator=false]
         * True to show a vertical icon separator line between the icons and the menu text.
         */
        separator: null,
 
        /**
         * @cfg {Boolean} [autoHide=true]
         * `false` to prevent the menu from auto-hiding when focus moves elsewhere
         */
        autoHide: null,
 
        /**
         * @cfg {Object} groups
         * This object is a dictionary of {@link Ext.menu.RadioItem#cfg!group radio group}
         * keys and {@link Ext.menu.RadioItem#cfg!value values}. This map is maintained by
         * the individual radio items in this menu but can also be useful for data binding.
         *
         * For example:
         *
         *      @example
         *      Ext.Viewport.add({
         *          xtype: 'container',
         *          items: [{
         *              xtype: 'button',
         *              bind: 'Call {menuGroups.option}',
         *               
         *              viewModel: {
         *                  data: {
         *                      menuGroups: {
         *                          option: 'home'
         *                      }
         *                  }
         *              },
         *               
         *              menu: {
         *                  bind: {
         *                      groups: '{menuGroups}'
         *                  },
         *                  items: [{
         *                      text: 'Home',
         *                      group: 'option',
         *                      value: 'home'
         *                  }, {
         *                      text: 'Work',
         *                      group: 'option',
         *                      value: 'work'
         *                  }, {
         *                      text: 'Mobile',
         *                      group: 'option',
         *                      value: 'mobile'
         *                  }]
         *              }
         *          }]
         *      });
         *
         * The presence of the `group` property in the configuration of the above
         * {@link Ext.menu.Menu menu} causes the menu to create a
         * {@link Ext.menu.RadioItem RadioItem} instances.
         */
        groups: null
    },
 
    /**
     * @cfg {Boolean} allowOtherMenus
     * True to allow multiple menus to be displayed at the same time.
     */
    allowOtherMenus: false,
 
    /**
     * @cfg {Boolean} ignoreParentClicks
     * True to ignore clicks on any item in this menu that is a parent item (displays a submenu)
     * so that the submenu is not dismissed when clicking the parent item.
     */
    ignoreParentClicks: false,
 
    /**
     * @cfg {Number} mouseLeaveDelay
     * The delay in ms as to how long the framework should wait before firing a mouseleave event.
     * This allows submenus not to be collapsed while hovering other menu items.
     */
    mouseLeaveDelay: 50,
    
    defaultType: 'menuitem',
 
    autoSize: null,
 
    twoWayBindable: 'groups',
 
    keyMap: {
        scope: 'this',
 
        // Space key clicks
        SPACE: 'onSpaceKey',
 
        // ESC hides
        ESC: 'onEscKey'
    },
 
    layout: {
        type: 'vbox',
        align: 'stretch'
    },
 
    classCls: Ext.baseCSSPrefix + 'menu',
    indentedCls: Ext.baseCSSPrefix + 'indented',
    hasSeparatorCls: Ext.baseCSSPrefix + 'has-separator',
    nonMenuItemCls: Ext.baseCSSPrefix + 'non-menuitem',
 
    // We need to focus disabled menu items when navigating as per WAI-ARIA:
    // http://www.w3.org/TR/wai-aria-practices/#menu
    allowFocusingDisabledChildren: true,
 
    border: true,
 
    // When a Menu is used as a carrier to float some focusable Component such as a
    // DatePicker or ColorPicker. This will be used to delegate focus to its focusable
    // child. In normal usage, a Menu is a FocusableContainer, and this will not be
    // consulted.
    defaultFocus: ':focusable',
 
    floated: true,
 
    // May be asked to focus, will delegate down to its first focusable child
    focusable: true,
 
    focusableContainer: true,
 
    nameHolder: true,
    weighted: true,
 
    /**
     * @event groupchange
     * Fires when a child {@link Ext.menu.RadioItem radio item} in a menu
     * {@link Ext.menu.RadioItem#cfg!group group} changes {@link Ext.menu.RadioItem#cfg!checked}
     * state, and the group's value therefore changes.
     *
     * The value changes to the {@link Ext.menu.RadioItem#cfg!value} of the sole checked
     * member of the group, or `null` if all members have become
     * {@link Ext.menu.RadioItem#cfg!allowUncheck unchecked}.
     *
     * @param {Ext.menu.Menu} menu The menu firing this event.
     * @param {String} groupName The name of the group of items.
     * @param {Object} newValue The new value of the group.
     * @param {Object} oldValue The old value of the group.
     * @since 6.5.1
     */
 
    initialize: function() {
        var me = this,
            listeners = {
                click: me.onClick,
                mouseover: me.onMouseOver,
                scope: me
            };
 
        me.callParent();
 
        if (Ext.supports.Touch) {
            listeners.pointerdown = me.onMouseOver;
        }
 
        me.element.on(listeners);
 
        // Child item mouseovers are handled on a delay so that
        // rapid movement down a menu does not activate/deactivate during mouse motion.
        // Also, allow for rapid reentry when user moves mouse quickly.
        me.itemOverTask = new Ext.util.DelayedTask(me.handleItemOver, me);
 
        me.mouseMonitor = me.el.monitorMouseLeave(me.mouseLeaveDelay, me.onMouseLeave, me);
    },
 
    doDestroy: function() {
        var me = this;
 
        // Cancel any impending mouseover consequences
        me.itemOverTask.cancel();
 
        // Menu can be destroyed while shown;
        // we should notify the Manager
        Ext.menu.Manager.onHide(me);
        
        me.parentMenu = me.ownerCmp = null;
 
        if (me.rendered) {
            me.el.un(me.mouseMonitor);
        }
        
        me.callParent();
    },
 
    showBy: function(component, alignment, options) {
        this.callParent([component, alignment || this.getAlign(), options]);
    },
 
    onFocusEnter: function(e) {
        var me = this,
            hierarchyState;
 
        me.callParent([e]);
 
        me.mixins.focusablecontainer.onFocusEnter.call(me, e);
 
        if (me.getFloated()) {
            hierarchyState = me.getInherited();
 
            // The topmost focusEnter event upon entry into a floating menu stack
            // is recorded in the hierarchy state.
            //
            // Focusing upwards from descendant menus in a stack will NOT trigger onFocusEnter
            // on the parent menu because focus is already in its component tree.
            // For focusing downwards we check for presence of the topmostFocusEvent
            // already being present in the hierarchy.
            //
            // If we need to explicitly access a focus reversion point, we can use that.
            // This is only ever needed if tabbing forwards from the menu. We explicitly
            // push focus to the topmost focusEnter component, and then allow natural
            // tabbing to proceed from there.
            //
            // In all other focus reversion scenarios we use the immediate focusEnter event
            if (!hierarchyState.topmostFocusEvent) {
                hierarchyState.topmostFocusEvent = e;
            }
        }
    },
 
    onFocusLeave: function(e) {
        this.callParent([e]);
        
        if (this.getAutoHide() !== false) {
            this.hide();
        }
    },
 
    onItemAdd: function(item, index) {
        this.callParent([item, index]);
 
        this.syncItemIndentedCls(item);
 
        if (!item.isMenuItem && !item.isMenuSeparator) {
            item.addCls(this.nonMenuItemCls);
        }
    },
 
    onItemRemove: function(item, index, destroying) {
        this.callParent([item, index, destroying]);
 
        item.removeCls(this.indentedCls, this.nonMenuItemCls);
    },
 
    beforeShow: function() {
        var me = this,
            parent;
 
        // If this is the topmost in a stack of menus, hide "other" menus
        // if we are configured not to tolerate other menus being visible.
        if (me.getFloated()) {
            parent = me.hasFloatMenuParent();
 
            if (!parent && !me.allowOtherMenus) {
                Ext.menu.Manager.hideAll();
            }
        }
 
        me.callParent(arguments);
    },
 
    afterShow: function() {
        var me = this,
            ariaDom = me.ariaEl.dom;
 
        me.callParent(arguments);
        Ext.menu.Manager.onShow(me);
 
        if (me.getFloated() && ariaDom) {
            ariaDom.setAttribute('aria-expanded', true);
        }
        
        // Restore configured maxHeight
        if (me.getFloated()) {
            me.maxHeight = me.savedMaxHeight;
        }
 
        if (me.autoFocus) {
            me.focus();
        }
    },
 
    afterHide: function() {
        var me = this,
            ariaDom = me.ariaEl.dom;
 
        me.callParent();
        me.lastHide = Ext.Date.now();
        Ext.menu.Manager.onHide(me);
 
        if (me.getFloated() && ariaDom) {
            ariaDom.setAttribute('aria-expanded', false);
        }
 
        // Top level focusEnter is only valid when focused
        delete me.getInherited().topmostFocusEvent;
    },
 
    factoryItem: function(cfg) {
        var result;
 
        if (typeof cfg === 'string' && cfg[0] !== '@') {
            if (cfg === '-') {
                cfg = { xtype: 'menuseparator' };
            }
            else {
                cfg = {};
            }
        }
 
        result = this.callParent([cfg]);
 
        if (result.isMenuItem) {
            result.parentMenu = this;
        }
 
        return result;
    },
 
    updateIndented: function(indented) {
        var me = this;
 
        if (!me.isConfiguring) {
            me.bodyElement.toggleCls(me.hasSeparatorCls, !!(indented && me.getSeparator()));
            me.items.each(me.syncItemIndentedCls, me);
        }
    },
 
    updateSeparator: function(separator) {
        this.bodyElement.toggleCls(this.hasSeparatorCls, !!(separator && this.getIndented()));
    },
 
    privates: {
        applyItemDefaults: function(item) {
            item = this.callParent([item]);
 
            if (!item.isComponent && !item.xtype && !item.xclass) {
                // If configured with group or name, then it's a RadioItem
                if (item.group || item.name) {
                    item.xtype = 'menuradioitem';
                }
                // The presence of a checked config defaults the type to a CheckItem
                else if ('checked' in item) {
                    item.xtype = 'menucheckitem';
                }
            }
 
            return item;
        },
 
        applyGroups: function(groups, oldGroups) {
            var me = this,
                currentGroups = Ext.apply({}, oldGroups),
                isConfiguring = me.isConfiguring,
                groupName, members, len, i, item, value, oldValue;
 
            if (groups) {
                me.updatingGroups = true;
 
                for (groupName in groups) {
                    oldValue = currentGroups[groupName];
                    currentGroups[groupName] = value = groups[groupName];
 
                    if (!isConfiguring) {
                        members = me.lookupName(groupName);
 
                        for (= 0, len = members.length; i < len; i++) {
                            item = members[i];
 
                            // Set checked state depending on whether the value is the group's value
                            item.setChecked(item.getValue() === value);
                        }
 
                        me.fireEvent('groupchange', me, groupName, value, oldValue);
                    }
                }
 
                me.updatingGroups = false;
            }
 
            return currentGroups;
        },
 
        processFocusableContainerKeyEvent: function(e) {
            var keyCode = e.keyCode,
                item;
 
            // FocusableContainer ignores events from input fields.
            // In Menus we have a special case. The ESC key, or arrow from 
            // <input type="checkbox"> must be handled.
            if (
                keyCode === e.ESC ||
                (
                    Ext.fly(e.target).is('input[type=checkbox]') && (
                        keyCode === e.LEFT ||
                        keyCode === e.RIGHT ||
                        keyCode === e.UP ||
                        keyCode === e.DOWN
                    )
                )
            ) {
                e.preventDefault();
                // TODO: we should never modify the "target" property of an event
                item = this.getItemFromEvent(e);
                e.target = item && item.focusEl.dom;
            }
            // TAB from textual input fields is converted into UP or DOWN.
            else if (keyCode === e.TAB && Ext.fly(e.target).is('input[type=text],textarea')) {
                e.preventDefault();
                // TODO: we should never modify the "target" property of an event
                item = this.getItemFromEvent(e);
                e.target = item && item.focusEl.dom;
 
                if (e.shiftKey) {
                    e.shiftKey = false;
                    e.keyCode = e.UP;
                }
                else {
                    e.keyCode = e.DOWN;
                }
            }
            else {
                return this.callParent([e]);
            }
 
            return e;
        },
 
        onEscKey: function(e) {
            if (this.getFloated()) {
                this.hide();
            }
        },
 
        onSpaceKey: function(e) {
            var clickedItem = this.getItemFromEvent(e);
 
            if (clickedItem && clickedItem.isMenuItem) {
                clickedItem.onSpace(e);
            }
        },
 
        onFocusableContainerLeftKey: function(e) {
            // The default action is to scroll the nearest horizontally scrollable container
            e.preventDefault();
 
            // Focus reversion will focus the activating MenuItem
            if (this.parentMenu) {
                this.hide();
            }
        },
 
        onFocusableContainerRightKey: function(e) {
            var clickedItem = this.getItemFromEvent(e);
 
            // The default action is to scroll the nearest horizontally scrollable container
            e.preventDefault();
 
            if (clickedItem) {
                clickedItem.expandMenu(e);
            }
        },
 
        onClick: function(e) {
            var me = this,
                type = e.type,
                clickedItem,
                clickResult,
                isKeyEvent = type === 'keydown',
                isTouchEvent = e.pointerType === 'touch';
 
            if (me.getDisabled()) {
                return e.stopEvent();
            }
 
            clickedItem = me.getItemFromEvent(e);
 
            if (clickedItem && clickedItem.isMenuItem) {
                if (!clickedItem.getMenu() || !me.ignoreParentClicks) {
                    clickResult = clickedItem.onClick(e);
                }
                else {
                    e.stopEvent();
                }
 
                // Click handler on the item could have destroyed the menu
                if (me.destroyed) {
                    return;
                }
 
                // SPACE and ENTER invokes the menu
                if (
                    clickedItem.getMenu() &&
                    clickResult !== false &&
                    (isKeyEvent || isTouchEvent)
                ) {
                    clickedItem.expandMenu(e);
                }
            }
 
            // Click event may be fired without an item, so we need a second check
            if (!clickedItem || clickedItem.getDisabled()) {
                clickedItem = undefined;
            }
 
            me.fireEvent('click', me, clickedItem, e);
        },
 
        onMouseLeave: function(e) {
            var me = this;
 
            if (me.itemOverTask) {
                me.itemOverTask.cancel();
            }
 
            if (me.getDisabled()) {
                return;
            }
 
            me.fireEvent('mouseleave', me, e);
        },
 
        /**
         * Handle either pointer moving over the menu's element, or, on 
         * touch capable devices, a touch start on the menu's element.
         */
        onMouseOver: function(e) {
            var me = this,
                activeItem = me.getActiveItem(),
                activeItemMenu = activeItem && activeItem.getMenu && activeItem.getMenu(),
                activeItemExpanded = activeItemMenu && activeItemMenu.isVisible(),
                isTouch = e.pointerType === 'touch',
                mouseEnter, overItem, el;
 
            if (!me.getDisabled()) {
                
                // If triggered by a touchstart, mouseenter is declared
                // if focus does not already reside within the menu.
                if (isTouch) {
                    mouseEnter = !me.el.contains(document.activeElement);
                }
                else {
                    mouseEnter = !me.el.contains(e.getRelatedTarget());
                }
 
                overItem = me.getItemFromEvent(e);
 
                // Focus the item in time specified by mouseLeaveDelay.
                // If we mouseout, or move to another item this invocation will be canceled.
                if (overItem) {
                    // pointerdown is routed to mouseover, handle pointerdown without delay
                    if (isTouch) {
                        me.handleItemOver(e, overItem);
                    }
                    else {
                        // ignore events on elements outside the bodyElement of menu items
                        // this ensures we don't apply mouseover styling when hovering the
                        // "separator" of a menu item, and we don't fire the menu item's
                        // handler when the separator is clicked.
                        el = overItem.isMenuItem ? overItem.bodyElement : overItem.el;
 
                        if (!el.contains(e.getRelatedTarget())) {
                            me.itemOverTask.delay(
                                activeItemExpanded ? me.mouseLeaveDelay : 0,
                                null,
                                null,
                                [e, overItem]
                            );
                        }
                    }
                }
 
                if (mouseEnter) {
                    me.fireEvent('mouseenter', me, e);
                }
 
                me.fireEvent('mouseover', me, overItem, e);
            }
        },
 
        /**
         * Handle the delayed consequences of pointer over a child menu.
         * Also called on touch start.
         */
        handleItemOver: function(e, item) {
            var isMouseover = e.pointerType === 'mouse';
 
            // We'll get here on touchstart on touch devices.
            // Only focus non-MenuItems on real mouseover events.
            if (!item.containsFocus && (isMouseover || item.isMenuItem)) {
                item.focus();
            }
 
            // Only expand the menu on real mouseover events.
            if (item.expandMenu && isMouseover) {
                item.expandMenu(e);
            }
        },
 
        /**
         * Gets the immediate child component which the passed event took place within
         */
        getItemFromEvent: function(e) {
            var bodyDom = this.bodyElement.dom,
                toEl = e.getTarget(),
                component;
 
            // See which immediate child element the event is in and find the
            // component which that element encapsulates.
            while (toEl && toEl.parentNode !== bodyDom) {
                toEl = toEl.parentNode;
            }
 
            component = toEl && Ext.getCmp(toEl.id);
 
            if (component && component.isMenuItem && !e.within(component.bodyElement)) {
                // ignore events on elements outside the bodyElement of menu items
                // this ensures we don't apply mouseover styling when hovering the
                // "separator" of a menu item, and we don't fire the menu item's
                // handler when the separator is clicked.
                component = null;
            }
 
            return component;
        },
 
        hasFloatMenuParent: function() {
            return this.parentMenu || this.up('menu[_floated=true]');
        },
 
        syncItemIndentedCls: function(item) {
            // menu items have an "indented" config
            // non menu items can have an "indented" property
            // The item's "indented" takes precedence over the menu's "indented"
            var indented = item.isMenuItem ? item.getIndented() : item.indented;
 
            item.toggleCls(this.indentedCls,
                           !!(indented || (this.getIndented() && (indented !== false))));
        }
    },
 
    statics: {
        /**
         * Returns a {@link Ext.menu.Menu} object
         * @param {Object/Object[]} menu An array of menu item configs,
         * or a Menu config that will be used to generate and return a new Menu.
         * @param {Object} [config] A configuration to use when creating the menu.
         * @return {Ext.menu.Menu} 
         */
        create: function(menu, config) {
            if (Ext.isArray(menu)) { // array of menu items
                menu = Ext.apply({ xtype: 'menu', items: menu }, config);
            }
            else {
                menu = Ext.apply({ xtype: 'menu' }, menu, config);
            }
 
            return Ext.create(menu);
        }
    },
 
    deprecated: {
        '6.5': {
            configs: {
                plain: {
                    message: 'To achieve classic toolkit "plain" effect, use "indented".'
                },
                showSeparator: {
                    message: 'To achieve classic toolkit "showSeparator" effect, use "separator".'
                }
            }
        }
    }
    
});