/*
 *  Copyright 2013 Anyware Services
 *
 *  Licensed under the Apache License, Version 2.0 (the "License");
 *  you may not use this file except in compliance with the License.
 *  You may obtain a copy of the License at
 *
 *      http://www.apache.org/licenses/LICENSE-2.0
 *
 *  Unless required by applicable law or agreed to in writing, software
 *  distributed under the License is distributed on an "AS IS" BASIS,
 *  WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *  See the License for the specific language governing permissions and
 *  limitations under the License.
 */

/**
 * This class controls a ribbon button.
 * 
 * - It can call a configured function when pressed.
 * - It supports enabling/disabling upon the current selection (see {@link #cfg-selection-target-id}) and associated rights (see {@link #cfg-rights}).
 * - It supports enabling/disabling upon a focused tool (see {@link #cfg-tool-id})
 * - It can be a toggle button (see {@link #cfg-toggle-enabled}).
 * 
 * Note that a property "controlId" is available on the created button. This string references this controller id, that can be retrieve with {@link Ametys.ribbon.RibbonManager#getUI}
 */
Ext.define(
	"Ametys.ribbon.element.ui.ButtonController",
	{
		extend: "Ametys.ribbon.element.RibbonUIController",
		
		mixins: { common: 'Ametys.ribbon.element.ui.CommonController' },
		
		/**
		 * @cfg {String} action A function to call when the button is pressed.
		 * Called with the following parameters:
		 * @cfg {Ametys.ribbon.element.ui.ButtonController} action.controller This button controller.
		 * @cfg {Boolean} action.state When the button is a toggle button, the new press-state of the button, null otherwise.
		 */

        /**
         * @readonly
         * @property {Boolean} ButtonController True if the instance is a Ametys.ribbon.element.ui.ButtonController instance
         */
        isButtonController: true,
		/**
		 * @cfg {Boolean/String} [toggle-enabled=false] When 'true', the button will be a toggle button. {@link #cfg-action} is still called, and the state can be retrieved using #isPressed 
		 */
		/**
		 * @property {Boolean} _toggleEnabled See ({@link #cfg-toggle-enabled})
		 * @private
		 */
		/**
		 * @cfg {Boolean/String} [toggle-state=false] When 'true', the button is created as pressed. Only available for toggle buttons (#cfg-toggle-enabled)=true.
		 */
		/**
		 * @property {Boolean} _pressed The current pressed state for a toggle button ({@link #cfg-toggle-enabled})
		 * @private
		 */
		
        /**
         * @cfg {Boolean/String} [sort-menu-items=false] When 'true', the items of a menu will be sorted by alphabetical order.
         */
        /**
         * @property {Boolean} _sortMenuItems See ({@link #sort-menu-items})
         * @private
         */
        
        /**
         * @cfg {Boolean/String} [sort-gallery-items=false] When 'true', the items of a gallery into a group will be sorted by alphabetical order.
         */
        /**
         * @property {Boolean} _sortGalleryItems See ({@link #sort-gallery-items})
         * @private
         */
        
		/**
		 * @property {Boolean/Object} [_refreshing=false] An object determining the refresh state or false if not refreshing
		 * @property {Boolean} _refreshing.disabled The _disabled member before the refreshing starts
		 */
		
		/**
		 * @property {String[]} _referencedControllerIds The ids of the other controllers referenced by this controller (such as the menu or gallery items)
		 * @private
		 */
		
		/**
		 * @cfg {Object} ui-config Additionnal configuration object to be passed to UI controls
		 */
		
		/**
		 * @private
		 * @property {Number} _refreshingCounter the counter on refreshing state.
		 */
		_refreshingCounter: 0,

        /**
         * @private
         * @property {Number} _nbGalleryItemsPerLine In the gallery, the number of gallery items to display on each line
         */
        
        /**
         * @private
         * @property {Number} __galleryItemWidth The width in pixels of a gallery item
         */
        _galleryItemWidth: 80,

		constructor: function(config)
		{
			this.callParent(arguments);
			this._toggleEnabled = this.getInitialConfig("toggle-enabled") == true || this.getInitialConfig("toggle-enabled") == 'true';
			this._pressed = this.getInitialConfig("toggle-state") == true || this.getInitialConfig("toggle-state") == 'true';
			this._refreshing = false;
            
            this._sortMenuItems = this.getInitialConfig("sort-menu-items") == true || this.getInitialConfig("sort-menu-items") == 'true';
            this._sortGalleryItems = this.getInitialConfig("sort-gallery-items") == true || this.getInitialConfig("sort-gallery-items") == 'true';
			
			this._referencedControllerIds = [];

            /**
             * @cfg {Number/String} nb-gallery-items-per-line The number of item to show by line. When not specified, will be computed automatically.
             */
            this._nbGalleryItemsPerLine = this.getInitialConfig("nb-gallery-items-per-line") ? parseInt(this.getInitialConfig("nb-gallery-items-per-line")) : null;

            /** @cfg {String/Boolean} display-filter Should the gallery display a filter? empty will kept automatic behavior */
            /** @property {Boolean} _displayToolbar See #cfg-display-toolbar */
            this._displayToolbar = this.getInitialConfig("display-toolbar") == null ? null : this.getInitialConfig("display-toolbar") == true || this.getInitialConfig("display-toolbar") == 'true';

			this._initialize(config);
		},
        
		createUI: function(size, colspan)
		{
			if (this.getLogger().isDebugEnabled())
			{
				this.getLogger().debug("Creating new UI button for controller " + this.getId() + " in size " + size + " (with colspan " + colspan + ")");
			}

			var hasGalleryItems = this.getInitialConfig("gallery-item") && this.getInitialConfig("gallery-item")["gallery-groups"].length > 0;
			var menuItemsCount = this._getMenuItemsCount();
			
            // FIXME RUNTIME-1539
//		    if (!hasGalleryItems && menuItemsCount == 1)
//		    {
//		    	// There is only one menu item => transform menu to one button
//		    	var menuItemCfg = this.getInitialConfig("menu-items");
//				for (var i=0; i < menuItemCfg.length; i++)
//				{
//					var elmt = Ametys.ribbon.RibbonManager.getUI(menuItemCfg[i]);
//					if (elmt != null)
//					{
//				    	return elmt.createUI (size, colspan);
//					}
//				}
//		    }
//		    else
//		    {
		    	var menu = this._getMenu();
				
				// Is this a split button, where the action is the one from a 'primary-menu-item-id' ?
				var primaryMenuItemId = this.getInitialConfig("primary-menu-item-id");
				var menuItemHandler = primaryMenuItemId && Ametys.ribbon.RibbonManager.hasUI(primaryMenuItemId) ? Ametys.ribbon.RibbonManager.getUI(primaryMenuItemId) : this;
				var isSplitButton = (this.getInitialConfig("action") != null || menuItemHandler.getInitialConfig("action") != null) && menu;

				var element = Ext.create(isSplitButton ? "Ametys.ui.fluent.ribbon.controls.SplitButton" : "Ametys.ui.fluent.ribbon.controls.Button", Ext.apply({
					text: this.getInitialConfig("label"),
					scale: size,
					colspan: colspan,
					iconCls: this._getIconCls(),
					icon: this._iconGlyph ? null : Ametys.CONTEXT_PATH + (size == 'large' ? this._iconMedium : this._iconSmall),
					tooltip: this._getTooltip(),
					
					handler: !this._toggleEnabled ? Ext.Function.createSequence(Ext.bind(this.toggleBackOnPress, this), Ext.bind(menuItemHandler.onPress, menuItemHandler)) : null,
					toggleHandler: this._toggleEnabled ? Ext.Function.createSequence(Ext.bind(this.toggleBackOnPress, this), Ext.bind(menuItemHandler.onPress, menuItemHandler)) : null,
                    // Note that the toggleBackOnPress is called with the original button, while the onPress is called with the menuItemHandler
					
					disabled: this._disabled,
					controlId: this.getId(),
					
					enableToggle: this._toggleEnabled,
					pressed: this._pressed,
					
					menu: menu
				}, this.getInitialConfig('ui-config') || {}));
                
//		    }
			
			return element;
		},
		
		createMenuItemUI: function ()
		{
			if (this.getLogger().isDebugEnabled())
			{
				this.getLogger().debug("Creating new UI menu item for controller " + this.getId());
			}

			var menu = this._getMenu();
			
			var element = Ext.create(this._toggleEnabled ? "Ext.menu.CheckItem" : "Ext.menu.Item", Ext.apply({
				text: this.getInitialConfig("label"),
                iconCls: this._getIconCls(),
				icon: !this._iconGlyph && this._iconSmall ? Ametys.CONTEXT_PATH + this._iconSmall : null,
				tooltip: this._getTooltip(false),
				showCheckbox: false,
				
				handler: !this._toggleEnabled ? Ext.Function.createSequence(Ext.bind(this.toggleBackOnPress, this), Ext.bind(this.onPress, this)) : null,
                checkHandler: this._toggleEnabled ? Ext.Function.createSequence(Ext.bind(this.toggleBackOnPress, this), Ext.bind(this.onPress, this)) : null,
                // Note that the toggleBackOnPress is called with the original button, while the onPress is called with the menuItemHandler

				disabled: this._disabled,
				controlId: this.getId(),
				
				checked: this._toggleEnabled ? this._pressed : null,
				
				hideOnClick: this.getInitialConfig("hide-on-click") || !this._toggleEnabled,
						
				menu: menu
			}, this.getInitialConfig('ui-config') || {}));

			return element;
		},
		
		createGalleryItemUI: function ()
		{
			if (this.getLogger().isDebugEnabled())
			{
				this.getLogger().debug("Creating new UI gallery item for controller " + this.getId());
			}

			var element = Ext.create("Ametys.ui.fluent.ribbon.controls.Button", Ext.apply({
		        text: this.getInitialConfig("label"),
		        tooltip: this._getTooltip(false),
		        
                iconCls: this._getIconCls(),
				icon: !this._iconGlyph && this._iconMedium ? Ametys.CONTEXT_PATH + this._iconMedium : null,
						
		        scale: 'large',
		        
		        handler: !this._toggleEnabled ? Ext.Function.createSequence(Ext.bind(this.toggleBackOnPress, this), Ext.bind(this.onPress, this)) : null,
                toggleHandler: this._toggleEnabled ? Ext.Function.createSequence(Ext.bind(this.toggleBackOnPress, this), Ext.bind(this.onPress, this)) : null,
				
				disabled: this._disabled,
				controlId: this.getId(),
				
				enableToggle: this._toggleEnabled,
				pressed: this._pressed
			}, this.getInitialConfig('ui-config') || {}));
			
			return element;
		},
		
		/**
		 * Get the menu constructed from button configuration. Call #_getMenuPanel to build items from galleries configuration. Call #_getMenuItems to build from items configuration.
		 * @return {Object/Ext.menu.Menu} A menu configuration or the menu itself. Can be null if there is no menu.
		 * @protected
		 */
		_getMenu: function ()
		{
			var items = [];
			
            var mp = this._getMenuPanel();
            if (mp)
            {
                items.push(mp);
            }
			
			var menuItems = this._getMenuItems();
			for (var i=0; i < menuItems.length; i++)
			{
				items.push(menuItems[i]);
			}
			
			if (items.length > 0)
			{
				var menu = new  Ext.create("Ext.menu.Menu", {
					cls: 'x-fluent-menu',
                    ui: "ribbon-menu",
					items: items
				});
                menu.on('show', Ext.bind(this._onMenuShowUpdateFilter, this));
                menu.on('hide', Ext.bind(this._onMenuHideClearFilter, this));
                menu.on('show', Ext.bind(this._onMenuShowComputeNbGalleryItemsPerLineColumnSize, this));

                return menu;
			}
			return null;
		},
		
		/**
		 * Get the menu panels from galleries configuration if exists
		 * @returns {Ametys.ui.fluent.ribbon.controls.gallery.MenuPanel} The menu panel having menu panels
		 * @protected
		 */
		_getMenuPanel: function ()
		{
            var me = this;
			var menuPanels = [];
			
			if (this.getInitialConfig("gallery-item"))
			{
				var galleryGroupsCfg = this.getInitialConfig("gallery-item")["gallery-groups"];
				
				for (var i=0; i < galleryGroupsCfg.length; i++)
				{
					var gpItems = [];
					
					var items = galleryGroupsCfg[i].items;
					for (var j=0; j < items.length; j++)
					{
						if (typeof (items[j]) == 'string')
						{
							var elmt = Ametys.ribbon.RibbonManager.getUI(items[j]);
							if (elmt != null)
							{
								gpItems.push(elmt.addGalleryItemUI());
								if (!Ext.Array.contains(this._referencedControllerIds, elmt.getId()))
								{
									this._referencedControllerIds.push(elmt.getId());
								}
								
							}
						}
						else if (items[j].className)
						{
							var elmt = Ext.create(items[j].className, Ext.applyIf(items[j].config, {id: this.getId() + '.group-.' + i + '-item' + j, pluginName: this.getPluginName()}));
							Ametys.ribbon.RibbonManager.registerUI(elmt);		
							gpItems.push(elmt.addGalleryItemUI());  
							
							if (!Ext.Array.contains(this._referencedControllerIds, elmt.getId()))
							{
								this._referencedControllerIds.push(elmt.getId());
							}
						}
					}
					
					if (gpItems.length > 0)
					{
                        if (this._sortGalleryItems)
                        {
                            gpItems.sort(this._compareGalleryItemText);
                        }
                        
						var menuPanelCfg = Ext.applyIf({
							title: galleryGroupsCfg[i].label,
							items: gpItems
						}, this._getMenuSubPanelConfig());
						
						var menuPanel = Ext.create("Ametys.ui.fluent.ribbon.controls.gallery.MenuPanel", menuPanelCfg);
						menuPanels.push(menuPanel);
					}
				}
			}

            if (menuPanels.length > 0)
            {
                return this._createGalleryWrapper(menuPanels);
            }
            else
            {
                return null;
            }
		},
        
        /**
         * @private
         * Creates the gallery wrapper
         * @param {Ametys.ui.fluent.ribbon.controls.gallery.MenuPanel[]} menuPanels The non-null aray of galleries to wrap
         * @return {Ext.Panel} The wrapper
         */
        _createGalleryWrapper: function(menuPanels)
        {
            var me = this;
            
            var dockedItems = [];
            if (this._displayToolbar !== false)
            {
                var items = [{
                    xtype: 'textfield',
                    flex: 1,
                    maxWidth: 4 * this._galleryItemWidth,
                    emptyText: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_RIBBON_SEARCHGALLERY_PLACEHOLDER}}",
                    listeners: {change: Ext.Function.createBuffered(this._searchFilter, 200, this)}
                }, {
                    // Clear filter
                    xtype: 'button',
                    itemId: 'clear',
                    tooltip: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_RIBBON_SEARCHGALLERY_CLEAR}}",
                    handler: Ext.bind (this._clearSearchFilter, this),
                    iconCls: 'a-btn-glyph ametysicon-eraser11', 
                    cls: 'a-btn-light'
                }, {
                    xtype: 'component',
                    flex: 0.5
                }];

                items.push({
                    xtype: 'button',
                    itemId: 'sizeswitch',
                    tooltip: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_RIBBON_SIZE_TOGGLE}}",
                    toggleHandler: Ext.bind (this._toggleSize, this),
                    enableToggle: true,
                    iconCls: 'a-btn-glyph ametysicon-list', // Beware, there is an icon switch in the toggleHandler 
                    cls: 'a-btn-light'
                });
                
                if (this._sortGalleryItems)
                {
                    // We can only toggle galleries if sort is activated
                    // indeed, when removing the categories we sort the buttons (if not the toggle is useless)
                    
                    items.push({
                        // Toggle categories
                        xtype: 'button',
                        itemId: 'categoryswitch',
                        tooltip: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_RIBBON_CATEGORIES_TOGGLE}}",
                        toggleHandler: Ext.bind (this._toggleCategories, this),
                        enableToggle: true,
                        iconCls: 'a-btn-glyph ametysicon-squares36', 
                        cls: 'a-btn-light'
                    });
                    
                    
                    // Categories, can be destroyer. Let's add the 'empty' category 
                    var menuPanelCfg = Ext.applyIf({
                        title: "{{i18n PLUGINS_CORE_UI_WORKSPACE_AMETYS_RIBBON_CATEGORIES_ALL}}",
                        hidden: true
                    }, this._getMenuSubPanelConfig());
                    
                    var menuPanel = Ext.create("Ametys.ui.fluent.ribbon.controls.gallery.MenuPanel", menuPanelCfg);
                    Ext.Array.insert(menuPanels, 0, [menuPanel]);
                }
                
                dockedItems.push({
                    xtype: 'container',
                    layout: {
                        type: 'hbox',
                        align: 'stretch'
                    },
                    dock: 'top',
                    items: items
                });
            }
            
            var p = Ext.create("Ext.Panel", {
                layout: 'anchor',
                defaults: {
                    ui: "ribbon-menu",
                    anchor: '100%'
                },
                dockedItems: dockedItems,
                flex: 1,
                isPanel: true,
                
                isMenuItem: true,
                onClick: Ext.emptyFn, // A menu item must have a click listener
                
                width: this._getMenuPanelWidth(),
                scrollable: "vertical",
                items: menuPanels,
                listeners: {
                    afterrender: function() {
                        this._wheelListener = this.el.on(
                            'mousewheel', Ext.bind(me._onGalleryMouseWheel, me, [this], true), me, { destroyable: true }
                        );
                    },
                    destroy: function() {
                        Ext.destroyMembers(this, '_wheelListener');
                    }
                }
            });
            return p;
        },
        
        /**
         * This listener is called on 'keyup' event on filter input field.
         * Filters the tree by text input.
         * @param {Ext.form.Field} field The field
         * @private
         */
        _searchFilter: function(field)
        {
            var value =  Ext.data.SortTypes.asNonAccentedUCString(field.getValue());

            var oneElement = field.up() // toolbar
                                .up() // gallery wrapper
                                .up() // menu 
                                .up(); // menu owner (button/parent menu)
            var oldY = oneElement.menu.getY();

            Ext.suspendLayouts();
            try
            {
                this._getGalleries(oneElement).each(function(gallery) {
                    var itemsVisible = 0; 
                    gallery.items.filterBy(function(button) { return button._filterable; }).each(function (button) {
                         if (Ext.data.SortTypes.asNonAccentedUCString(button.getText()).indexOf(value) == -1)
                         {
                            button.hide();
                         }
                         else
                         {
                            itemsVisible++;
                            button.show();
                         }
                    });
                    
                    if (itemsVisible > 0)
                    {
                        gallery.show();
                    }
                    else
                    {
                        gallery.hide();
                    }
                });
            }
            finally
            {
                Ext.resumeLayouts(true);
                
                // changing stuffs may move the item vertically
                oneElement.menu.setY(oldY);
            }
        },
        /**
         * Clear the filter search
         * @param {Ext.Button} btn The button
         * @private
         */
        _clearSearchFilter: function(btn)
        {
            var filter = btn.prev(); 
            filter.reset();
        },
        
        /**
         * @private
         * Switch from big to small size
         * @param {Ext.Button} button The pressed button
         * @param {boolean} toggleState The new state
         */
        _toggleSize: function(button, toggleState)
        {
            var me = this;
            
            button.setIconCls(toggleState ? "a-btn-glyph ametysicon-grid" : "a-btn-glyph ametysicon-list")
            
            var oneElement = button.up() // toolbar
                                    .up() // gallery wrapper
                                    .up() // menu 
                                    .up(); // menu owner (button/parent menu)
            var oldY = oneElement.menu.getY();
            try
            {
                Ext.suspendLayouts();
                
                this._getGalleries(oneElement).each(function (category) {
                    category.items.each(function (button) {
                        var isVisible = button.isVisible();
                        if (!isVisible)
                        {
                            button.show(); // Changing width on a hidden button is not correctly applied
                        }
                        button.setScale(toggleState ? "small" : "large");
                        button.setWidth(toggleState ? "100%" : me._galleryItemWidth);
                        button.setTextAlign(toggleState ? "left" : "center")
                        if (!isVisible)
                        {
                            button.hide();
                        }
                    });
                })
            }
            finally
            {
                Ext.resumeLayouts(true);
                
                // changing stuffs may move the item vertically
                oneElement.menu.setY(oldY);
            }
        },
        
        /**
         * @private
         * Display or hide the galleries categories
         * @param {Ext.Button} button The pressed button
         * @param {boolean} toggleState The new state
         */
        _toggleCategories: function(button, toggleState)
        {
            var oneElement = button.up() // toolbar
                                    .up() // gallery wrapper
                                    .up() // menu 
                                    .up(); // menu owner (button/parent menu)
            var oldY = oneElement.menu.getY();
            var categories = this._getGalleries(oneElement);
            var allCategory = categories.first();
            
            try
            {
                Ext.suspendLayouts();
                
                if (toggleState)
                {
                    // Remove categories
                    allCategory.show();
                    
                    var items = [];
                    
                    // Get buttons to move (and hide old parents)
                    categories.each(function (category) {
                        if (category != allCategory)
                        {
                            category.items.each(function (b) {
                                b._origin = category.getId(); 
                                items.push(b);
                            });
                            category.hide();
                        }
                    });
                    
                    // Reorder
                    items.sort(this._compareGalleryItemText);
                    // Insert
                    for (var i = 0; i < items.length; i++)
                    {
                        allCategory.add(items[i])
                    }
                }
                else
                {
                    // Restore categories
                    allCategory.hide();
                    
                    allCategory.items.each(function (b) {
                        var category = Ext.getCmp(b._origin);
                        if (b.isVisible())
                        {
                            category.show();
                        }
                        category.add(b);
                    });
                    
                    // No need to resort items
                }
            }
            finally
            {
                Ext.resumeLayouts(true);
                
                // changing stuffs may move the item vertically
                oneElement.menu.setY(oldY);
            }
        },
        
        
        /**
         * When mouse is scrolled in gallery
         * @param {Event} e The scroll event
         * @param {HTMLElement} target The target of event
         * @param {Object} eOpt The options of the event 
         * @param {Ext.Element} elt The source element
         */
        _onGalleryMouseWheel: function(e, target, eOpt,  elt)
        {
            var cmp = Ext.Component.from(e.target),
                cmpScroller = cmp.getScrollable && cmp.getScrollable();
    
            // Only stop the event if we are not scrolling a scrollable component
            // inside this container.
            if (!cmpScroller || (cmpScroller === this.layout.owner.getScrollable())) 
            {
                e.stopEvent();
                elt.scrollBy(0, e.getWheelDelta(), true);
            }
        },
        
        /**
         * Comparison function for two gallery items based on item' text.
         * @param {Ext.menu.Item} gpItem1 The first item to compare
         * @param {Ext.menu.Item} gpItem2 The second item to compare with
         * @return {Number}
         */
        _compareGalleryItemText: function (gpItem1, gpItem2)
        {
            var text1 = Ext.data.SortTypes.asNonAccentedUCString (gpItem1.getText() || gpItem1.text || '');   
            var text2 = Ext.data.SortTypes.asNonAccentedUCString (gpItem2.getText() || gpItem2.text || '');  
            
            if (text1 == text2)
            {
                return 0;
            }
            else
            {
                return text1 < text2 ? -1 : 1;
            }
        },
        
		/**
		 * Get the configuration of each menu panel from initial configuration if it exists. Called by #_getMenuPanel.
		 * @return {Object} The menu panel configuration object
		 * @protected
		 */
		_getMenuSubPanelConfig: function()
		{
			return {
				defaults: {
					width: this._galleryItemWidth
				}
			};
		},
        
        /**
         * @private
         * Dertermines if the given element is a gallery wrapper
         * @param {Ext.Component} element The element to test
         * @return {Boolean} true is it a gallery wrapper
         */
        _isGalleriesWrapper: function(element)
        {
            return element.isPanel;
        },
        
        /**
         * @private
         * Dertermines if the given element is a gallery
         * @param {Ext.Component} element The element to test
         * @return {Boolean} true is it a gallery
         */
        _isGallery: function(element)
        {
            return element.isPanel;
        },
        
        /**
         * @protected
         * @param {Ametys.ui.fluent.ribbon.controls.SplitButton/Ametys.ui.fluent.ribbon.controls.Button/Ext.menu.CheckItem/Ext.menu.Item} [element] To limit to one ui button. When null will loop on every button.
         * @returns {Ext.util.MixedCollection} Collection of Ext.Container
         */
        _getGalleriesWrappers: function(element)
        {
            var me = this;
            
            var elements;
            if (element)
            {
                elements = new Ext.util.MixedCollection();
                elements.add(element);
            }
            else
            {
                elements = this.getUIControls();
            }

            var galleriesWrappers = new Ext.util.MixedCollection();
            elements.each(function(element) {
                if (element.getMenu() && element.getMenu().items)
                {
                    element.getMenu().items.filterBy(me._isGalleriesWrapper, me).each(function (galleryWrapper) {
                        galleriesWrappers.add(galleryWrapper);
                    });
                }
            });
            return galleriesWrappers; 
        },
        
        /**
         * @protected
         * Get the gallery panels
         * @param {Ametys.ui.fluent.ribbon.controls.SplitButton/Ametys.ui.fluent.ribbon.controls.Button/Ext.menu.CheckItem/Ext.menu.Item} [element] To limit to one ui button. When null will loop on every button.
         * @returns {Ext.util.MixedCollection} Collection of Ametys.ui.fluent.ribbon.controls.gallery.MenuPanel
         */
        _getGalleries: function(element)
        {
            var me = this;
            var galleries = new Ext.util.MixedCollection();

            this._getGalleriesWrappers(element).each(function(galleryWrapper) {
                galleryWrapper.items.filterBy(me._isGallery, me).each(function (gallery) {
                    galleries.add(gallery);
                });
            });
            
            return galleries;
        },
        
        /**
         * @private
         * Listener "on menu hide" to clear the filter
         * @param {Ext.Component} menu The menu
         */
        _onMenuHideClearFilter: function(menu)
        {
            var oneElement = menu.up();
            
            if (this._displayToolbar === false)
            {
                return;
            }
            
            var galleriesWrappers = this._getGalleriesWrappers(oneElement);
            if (galleriesWrappers.length == 0)
            {
                // No gallery
                return;
            }
            
            var toolbar =  galleriesWrappers.get(0) // only one galleryWrapper when providing an element
                                            .getDockedItems()[0]; // get the top dockbar
            
            if (toolbar.isVisible())
            {
                this._clearSearchFilter(toolbar.getComponent("clear"));
                
                // Untoggle the button
                toolbar.getComponent("sizeswitch").toggle(false);

                if (this._sortGalleryItems)
                {
                    // Untoggle the button
                    toolbar.getComponent("categoryswitch").toggle(false);
                }
            }
        },
        
        /**
         * @private
         * Listener "on menu show" to show and prepare the filter
         * @param {Ext.Component} menu The menu
         */
        _onMenuShowUpdateFilter: function(menu)
        {
            var oneElement = menu.up();
            
            if (this._displayToolbar === false)
            {
                return;
            }
            
            var galleriesWrappers = this._getGalleriesWrappers(oneElement);
            if (galleriesWrappers.length == 0)
            {
                // No gallery
                return;
            }
            
            var toolbar =  galleriesWrappers.get(0) // only one galleryWrapper when providing an element
                                            .getDockedItems()[0]; // get the top dockbar
            
            // For each gallery, how many visible buttons?
            var galleriesCount = 0;
            var galleriesItemsCount = 0;

            var g = this._getGalleries(oneElement);
            if (g.getCount() == 0)
            {
                // No gallery in the wrapper return
                toolbar.hide();
                return;
            }
            g.each(function(gallery) {
                if (gallery.isVisible())
                {
                    gallery.items.each(function(button) {
                        button._filterable = false;
                        if (button.isVisible())
                        {
                            button._filterable = true;
                            galleriesItemsCount ++;
                        }
                    });
                    
                    galleriesCount ++;
                    
                }
            });
            
            if (this._displayToolbar !== true                          // Unless display filter is forced 
                && galleriesCount <= 2 && galleriesItemsCount <= 10)  // few items does not need a filter 
            {
                toolbar.hide();
                return;
            }
            toolbar.show();
            
            var filterInput = toolbar.items.get(0);   // The toolbar have the filter input and the clear button
            filterInput.focus();
        },
        
        /**
         * @private
         * Listener "on menu show" to compute the gallery size
         * @param {Ext.Component} menu The menu
         */
        _onMenuShowComputeNbGalleryItemsPerLineColumnSize: function(menu)
        {
            var oneElement = menu.up();
            
            var me = this;
            
            if (this._nbGalleryItemsPerLine)
            {
                // The value was fixed. Do not compute
                return;
            }

            if (!oneElement.menu.items)
            {
                // No menu. Nothing to compute
                return;
            }
            
            // For each gallery, how many visible buttons?
            var galleriesItems = [];
            
            var g = this._getGalleries(oneElement);
            if (g.getCount() == 0)
            {
                // No gallery return
                return;
            }
            g.each(function(gallery) {
                if (gallery.isVisible())
                {
                    var nbVisibleItems = gallery.items.filterBy(function(b) { return b.isVisible(); }).getCount();
                    galleriesItems.push(nbVisibleItems);
                }
            });
            
            // Min/Max item per lines
            var minIconsPerLine = 5;
            var maxIconsPerLine;
            
            // Compute max zone in px
            var toolZone = Ext.ComponentQuery.query('viewport')[0].items.get(1); // Item #2 of viewport is the tool zone
            var shouldFitInWidth = toolZone.getWidth() - 318 /* tip on sides */ - 50 /* to be sure */;
            var nbButtonsThatFitInWidth = Math.max(minIconsPerLine, Math.floor((shouldFitInWidth) / this._galleryItemWidth)); // buttons that fit in width... but there is a min
            var shouldFitInHeight = toolZone.getHeight() - 10 /* to be sure*/;

            // Seek for big sections
            var containsBigSection = false;
            if (galleriesItems.length == 0)
            {
                // there is no gallery ?!
                maxIconsPerLine = minIconsPerLine;
            }
            else
            {
                // By default, to avoid visual gap, the smallest line should not have more than 5 empty spaces
                maxIconsPerLine = Math.min(10, Ext.min(galleriesItems) + 5, nbButtonsThatFitInWidth); // Max is between minIconsPerLine and the minimal
            
                if (Ext.max(galleriesItems) / maxIconsPerLine > 3)
                {
                    containsBigSection = true;
                    // When there is a least a big section, we accept a bigger visual gap
                    maxIconsPerLine = Math.min(10, Ext.min(galleriesItems) + 7, nbButtonsThatFitInWidth); // Max is between minIconsPerLine and the minimal
                }
            }
            
            // Compute the minimal height that fit
            var minHeight;
            for (var j = minIconsPerLine; j <= maxIconsPerLine; j++)
            {
                var height = computeGalleryHeight(j);
                
                if (!minHeight || height < minHeight.height) // On same height, min area is better
                {
                    minHeight = { height: height, nbItemPerLine: j };
                }
                
                if (height < shouldFitInHeight && !containsBigSection)
                {
                    break;
                }
            }

            // UI update
            var oldY = oneElement.menu.getY();
            var newWidth = this._getMenuPanelWidth(minHeight.nbItemPerLine);
            this._getGalleriesWrappers(oneElement).each(function (menuItem) {
                menuItem.setWidth(newWidth + 20);
            });
            
            // changing width may move the item vertically
            oneElement.menu.updateLayout(); // sometimes the height is not correctly updated 
            oneElement.menu.setY(oldY);

            function computeGalleryHeight(nbItemPerLine)
            {
                var height = 0;
                
                for (var i = 0; i < galleriesItems.length; i++)
                {
                    height += 22; // Header size
                    height += Math.ceil(galleriesItems[i] / nbItemPerLine) * 78; // Buttons
                    height += 3; // Footer
                }
                
                return height;
            }
        },
        
        /**
         * @private
         * Get the width for the galleries
         */
        _getMenuPanelWidth: function(nbGalleryItemsPerLine)
        {
            var num = nbGalleryItemsPerLine || this._nbGalleryItemsPerLine; 
            return num ? this._galleryItemWidth * num + 4 : null;
        },
		
        // TO remove
		_getMenuItemsCount: function ()
		{
			var count = 0;
			
			if (this.getInitialConfig("menu-items"))
			{
				var menuItemCfg = this.getInitialConfig("menu-items");
				
				for (var i=0; i < menuItemCfg.length; i++)
				{
					var elmt = Ametys.ribbon.RibbonManager.getUI(menuItemCfg[i]);
					if (elmt != null)
					{
						count++;
					}
				}
			}
			
			return count;
		},
		
		/**
		 * Get the menu items from menu items configuration if it exists
		 * @returns {Ext.menu.Item[]} The menu items
		 * @protected
		 */
		_getMenuItems: function ()
		{
			var menuItems = [];
			
			if (this.getInitialConfig("menu-items"))
			{
				var menuItemCfg = this.getInitialConfig("menu-items");
				
				for (var i=0; i < menuItemCfg.length; i++)
				{
					var elmt = Ametys.ribbon.RibbonManager.getUI(menuItemCfg[i]);
					if (elmt != null)
					{
						menuItems.push(elmt.addMenuItemUI());
						if (!Ext.Array.contains(this._referencedControllerIds, elmt.getId()))
						{
							this._referencedControllerIds.push(elmt.getId());
						}
					}
				}
			}
            
            if (this._sortMenuItems)
            {
                // Sort items by alphabetical order
                menuItems.sort(this._compareMenuItemText);
            }
            
			return menuItems;
		},
        
        /**
         * Comparison function for two menu items based on item' text.
         * @param {Ext.menu.Item} item1 The first item to compare
         * @param {Ext.menu.Item} item2 The second item to compare with
         * @return {Number}
         */
        _compareMenuItemText: function (item1, item2)
        {
            var text1 = Ext.data.SortTypes.asNonAccentedUCString (item1.text || '');   
            var text2 = Ext.data.SortTypes.asNonAccentedUCString (item2.text || '');  
            
            if (text1 == text2)
            {
                return 0;
            }
            else
            {
                return text1 < text2 ? -1 : 1;
            }
        },
		
		/**
		 * Update the ui controls for images and tooltip
		 * @protected
		 */
		_updateUI: function()
		{
			var me = this;
			this.getUIControls().each(function (element) {
				if (!me._iconGlyph && element instanceof Ext.button.Button)
				{
					if (element.scale == 'large')
					{
						element.setIcon(Ametys.CONTEXT_PATH + me._iconMedium);
					}
					else
					{
						element.setIcon(Ametys.CONTEXT_PATH + me._iconSmall);
					}
				}
				else if (me._iconGlyph)
				{
					element.setIconCls(me._getIconCls())
				}
				
				var isNotInRibbon = element.ownerCt instanceof Ametys.ui.fluent.ribbon.controls.gallery.MenuPanel 
								 || element instanceof Ext.menu.Item; 
				element.setTooltip(me._getTooltip(!isNotInRibbon));
			});			
		},
		
		/**
		 * Get the ids of others controllers referenced by this controller (such as the menu or gallery items)
		 * @return {String[]} the ids of others controllers
		 */
		getReferencedControllerIds: function ()
		{
			return this._referencedControllerIds;
		},
        
        /**
         * @private
         * Additionnal handler for toggle buttons only
         * @param {Ametys.ui.fluent.ribbon.controls.Button} button The pressed button
         * @param {Boolean} state When the button is a toggle button, the new press-state of the button
         */
        toggleBackOnPress: function(button, state)
        {
			// ensure, all UIs are coherent
            // At this time, we do not apply the new press-state, the action will have to it by it-self.
            // Note, that we will call toggle event on non toggle buttons, because lot's of button forgot to be declared as togglable
			this.toggle(this._pressed); 
        },
		
		/**
		 * Handler for the button. The default behavior is to call the function defined in #cfg-action
		 * @param {Ametys.ui.fluent.ribbon.controls.Button} button The pressed button
		 * @param {Boolean} state When the button is a toggle button, the new press-state of the button
		 * @protected
		 */
		onPress: function(button, state)
		{
			if (this.getLogger().isInfoEnabled())
			{
				this.getLogger().info("Pressing button " + this.getId() + "");
			}
			
			var actionFn = this.getInitialConfig("action");
			if (actionFn)
			{
				if (this.getLogger().isDebugEnabled())
				{
					this.getLogger().debug("Calling action for button " + this.getId() + ": " + actionFn);
				}
				
				Ametys.executeFunctionByName(actionFn, null, null, this, this._toggleEnabled ? state : null);
			}
		},
		
		/**
		 * Get the current controller state (pressed or not) for a toggle button controller (#cfg-toggle-enabled is 'true').
		 * @returns {Boolean} The state
		 */
		isPressed: function()
		{
			return this._pressed;
		},
		
		/**
		 * When the button is a toggle button (#cfg-toggle-enabled is 'true') this allow to change all controlled UIs state.
		 * No event is thrown on the UIs
		 * @param {Boolean} [state] Change the state to the current value. When not specified the new state is the opposite of the current state.
		 */
		toggle: function(state)
		{
			var me = this;
            
            if (state && !this._toggleEnabled && this.getLogger().isWarnEnabled())
            {
                this.getLogger().warn("Controller '" + this.getId() + "' is using #toggle but is not <toggle-enabled>true</toggle-enabled>")
            }

			if (Ext.isBoolean(state))
			{
				this._pressed = state;
			}
			else
			{
				this._pressed = !this._pressed;
			}
			
			this.getUIControls().each(function (elmt) {
				if (elmt instanceof Ext.Button)
				{
                    elmt.enableToggle = true; // Since ExtJS 6.5.1, we force the configuration that should have been declared
					elmt.toggle(me._pressed, true);
				}
				else if (elmt instanceof Ext.menu.CheckItem)
				{
					elmt.setChecked(me._pressed, true);
				}
			});
		},
		
		/**
		 * Set all controlled UIs in a refreshing state. See #stopRefreshing
		 */
		refreshing: function ()
		{
			this._refreshingCounter++;
			
			var currentDisableState = this._disabled;
			this.disable();
			this._refreshing = {
					disabled: currentDisableState
			};
			
			this.getUIControls().each(function (elmt) {
				if (elmt instanceof Ext.Button)
				{
					elmt.refreshing();
				}
			});
		},

        /**
         * Stop the refreshing state for all controlled UIs. See #refreshing
         * @param [force=false] When false, stopRefreshing will only stop if the counter of refreshing is 0 ; when true, put the counter to 0
         */
        stopRefreshing: function (force)
        {
            if (this._refreshingCounter != 0)
            {
                this._refreshingCounter = force ? 0 : Math.max(0, this._refreshingCounter - 1);
                
                // Do NOT stop refreshing until a refresh is still in progress
                if (this._refreshingCounter == 0)
                {
                    this.getUIControls().each(function (elmt) {
                        if (elmt instanceof Ext.Button)
                        {
                            elmt.stopRefreshing();
                        }
                    });
                    
                    var oldDisableState = this._refreshing.disabled; 
                    this._refreshing = false;
                    this.setDisabled(oldDisableState);
                }
            }
        },
		
        updateState: function(selectionChanged, toolStatusChanged, toolDirtyStateChanged)
        {
            this.stopRefreshing(true);
            
            this.mixins.common.updateState.apply(this, arguments)
        },  
		
		disable: function()
		{
			if (!this._refreshing)
			{
				this.mixins.common.disable.apply(this);
			}
			else
			{
				this._refreshing.disabled = true;
			}
		},
		
		enable: function()
		{
			if (!this._refreshing)
			{
				this.mixins.common.enable.apply(this);
			}
			else
			{
				this._refreshing.disabled = false;
			}
		},
        
        isDisabled: function()
        {
            if (!this._refreshing)
            {
                return this.mixins.common.isDisabled.apply(this);
            }
            else
            {
                return this._refreshing.disabled;
            }
        },
		
        /**
         * Called to prepare options for a #serverCall 
         * @param {Object} options See #serverCall for default options. This implementation additionnal have the following properties
         * @param {Boolean} [options.refreshing=false] When 'true', the button will automatically call #refreshing and #stopRefreshing before and after the server request.
         */
        beforeServerCall: function(options)
        {
            if (options.refreshing == true)
            {
                this.refreshing();
            }
        },
        
        /**
         * @inheritDoc
         * @private 
         */
        afterServerCall: function(serverResponse, options)
        {
            if (options.refreshing == true)
            {
                this.stopRefreshing();
            }
        },
        
        /**
         * @inheritDoc
         * @private 
         */
        afterCancelledServerCall: function (options)
        {
        	if (options.refreshing == true)
        	{
        		this.stopRefreshing();
        	}
        }
	}
);