/**
 * Toolbars are complex interface components that permit users to easily access a variety
 * of {@link OO.ui.Tool tools} (e.g., formatting commands) and actions, which are additional
 * commands that are part of the toolbar, but not configured as tools.
 *
 * Individual tools are customized and then registered with a
 * {@link OO.ui.ToolFactory tool factory}, which creates the tools on demand. Each tool has a
 * symbolic name (used when registering the tool), a title (e.g., ‘Insert image’), and an icon.
 *
 * Individual tools are organized in {@link OO.ui.ToolGroup toolgroups}, which can be
 * {@link OO.ui.MenuToolGroup menus} of tools, {@link OO.ui.ListToolGroup lists} of tools, or a
 * single {@link OO.ui.BarToolGroup bar} of tools. The arrangement and order of the toolgroups is
 * customized when the toolbar is set up. Tools can be presented in any order, but each can only
 * appear once in the toolbar.
 *
 * The toolbar can be synchronized with the state of the external "application", like a text
 * editor's editing area, marking tools as active/inactive (e.g. a 'bold' tool would be shown as
 * active when the text cursor was inside bolded text) or enabled/disabled (e.g. a table caption
 * tool would be disabled while the user is not editing a table). A state change is signalled by
 * emitting the {@link OO.ui.Toolbar#event:updateState 'updateState' event}, which calls Tools'
 * {@link OO.ui.Tool#onUpdateState onUpdateState method}.
 *
 *     @example <caption>The following is an example of a basic toolbar.</caption>
 *     // Example of a toolbar
 *     // Create the toolbar
 *     const toolFactory = new OO.ui.ToolFactory();
 *     const toolGroupFactory = new OO.ui.ToolGroupFactory();
 *     const toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
 *
 *     // We will be placing status text in this element when tools are used
 *     const $area = $( '<p>' ).text( 'Toolbar example' );
 *
 *     // Define the tools that we're going to place in our toolbar
 *
 *     // Create a class inheriting from OO.ui.Tool
 *     function SearchTool() {
 *         SearchTool.super.apply( this, arguments );
 *     }
 *     OO.inheritClass( SearchTool, OO.ui.Tool );
 *     // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
 *     // of 'icon' and 'title' (displayed icon and text).
 *     SearchTool.static.name = 'search';
 *     SearchTool.static.icon = 'search';
 *     SearchTool.static.title = 'Search...';
 *     // Defines the action that will happen when this tool is selected (clicked).
 *     SearchTool.prototype.onSelect = function () {
 *         $area.text( 'Search tool clicked!' );
 *         // Never display this tool as "active" (selected).
 *         this.setActive( false );
 *     };
 *     SearchTool.prototype.onUpdateState = function () {};
 *     // Make this tool available in our toolFactory and thus our toolbar
 *     toolFactory.register( SearchTool );
 *
 *     // Register two more tools, nothing interesting here
 *     function SettingsTool() {
 *         SettingsTool.super.apply( this, arguments );
 *     }
 *     OO.inheritClass( SettingsTool, OO.ui.Tool );
 *     SettingsTool.static.name = 'settings';
 *     SettingsTool.static.icon = 'settings';
 *     SettingsTool.static.title = 'Change settings';
 *     SettingsTool.prototype.onSelect = function () {
 *         $area.text( 'Settings tool clicked!' );
 *         this.setActive( false );
 *     };
 *     SettingsTool.prototype.onUpdateState = function () {};
 *     toolFactory.register( SettingsTool );
 *
 *     // Register two more tools, nothing interesting here
 *     function StuffTool() {
 *         StuffTool.super.apply( this, arguments );
 *     }
 *     OO.inheritClass( StuffTool, OO.ui.Tool );
 *     StuffTool.static.name = 'stuff';
 *     StuffTool.static.icon = 'ellipsis';
 *     StuffTool.static.title = 'More stuff';
 *     StuffTool.prototype.onSelect = function () {
 *         $area.text( 'More stuff tool clicked!' );
 *         this.setActive( false );
 *     };
 *     StuffTool.prototype.onUpdateState = function () {};
 *     toolFactory.register( StuffTool );
 *
 *     // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
 *     // little popup window (a PopupWidget).
 *     function HelpTool( toolGroup, config ) {
 *         OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
 *             padded: true,
 *             label: 'Help',
 *             head: true
 *         } }, config ) );
 *         this.popup.$body.append( '<p>I am helpful!</p>' );
 *     }
 *     OO.inheritClass( HelpTool, OO.ui.PopupTool );
 *     HelpTool.static.name = 'help';
 *     HelpTool.static.icon = 'help';
 *     HelpTool.static.title = 'Help';
 *     toolFactory.register( HelpTool );
 *
 *     // Finally define which tools and in what order appear in the toolbar. Each tool may only be
 *     // used once (but not all defined tools must be used).
 *     toolbar.setup( [
 *         {
 *             // 'bar' tool groups display tools' icons only, side-by-side.
 *             type: 'bar',
 *             include: [ 'search', 'help' ]
 *         },
 *         {
 *             // 'list' tool groups display both the titles and icons, in a dropdown list.
 *             type: 'list',
 *             indicator: 'down',
 *             label: 'More',
 *             include: [ 'settings', 'stuff' ]
 *         }
 *         // Note how the tools themselves are toolgroup-agnostic - the same tool can be displayed
 *         // either in a 'list' or a 'bar'. There is a 'menu' tool group too, not showcased here,
 *         // since it's more complicated to use. (See the next example snippet on this page.)
 *     ] );
 *
 *     // Create some UI around the toolbar and place it in the document
 *     const frame = new OO.ui.PanelLayout( {
 *         expanded: false,
 *         framed: true
 *     } );
 *     const contentFrame = new OO.ui.PanelLayout( {
 *         expanded: false,
 *         padded: true
 *     } );
 *     frame.$element.append(
 *         toolbar.$element,
 *         contentFrame.$element.append( $area )
 *     );
 *     $( document.body ).append( frame.$element );
 *
 *     // Here is where the toolbar is actually built. This must be done after inserting it into the
 *     // document.
 *     toolbar.initialize();
 *     toolbar.emit( 'updateState' );
 *
 *     @example <caption>The following example extends the previous one to illustrate 'menu' toolgroups and the usage of
 *     {@link OO.ui.Toolbar#event:updateState 'updateState' event}.</caption>
 *     // Create the toolbar
 *     const toolFactory = new OO.ui.ToolFactory();
 *     const toolGroupFactory = new OO.ui.ToolGroupFactory();
 *     const toolbar = new OO.ui.Toolbar( toolFactory, toolGroupFactory );
 *
 *     // We will be placing status text in this element when tools are used
 *     const $area = $( '<p>' ).text( 'Toolbar example' );
 *
 *     // Define the tools that we're going to place in our toolbar
 *
 *     // Create a class inheriting from OO.ui.Tool
 *     function SearchTool() {
 *         SearchTool.super.apply( this, arguments );
 *     }
 *     OO.inheritClass( SearchTool, OO.ui.Tool );
 *     // Each tool must have a 'name' (used as an internal identifier, see later) and at least one
 *     // of 'icon' and 'title' (displayed icon and text).
 *     SearchTool.static.name = 'search';
 *     SearchTool.static.icon = 'search';
 *     SearchTool.static.title = 'Search...';
 *     // Defines the action that will happen when this tool is selected (clicked).
 *     SearchTool.prototype.onSelect = function () {
 *         $area.text( 'Search tool clicked!' );
 *         // Never display this tool as "active" (selected).
 *         this.setActive( false );
 *     };
 *     SearchTool.prototype.onUpdateState = function () {};
 *     // Make this tool available in our toolFactory and thus our toolbar
 *     toolFactory.register( SearchTool );
 *
 *     // Register two more tools, nothing interesting here
 *     function SettingsTool() {
 *         SettingsTool.super.apply( this, arguments );
 *         this.reallyActive = false;
 *     }
 *     OO.inheritClass( SettingsTool, OO.ui.Tool );
 *     SettingsTool.static.name = 'settings';
 *     SettingsTool.static.icon = 'settings';
 *     SettingsTool.static.title = 'Change settings';
 *     SettingsTool.prototype.onSelect = function () {
 *         $area.text( 'Settings tool clicked!' );
 *         // Toggle the active state on each click
 *         this.reallyActive = !this.reallyActive;
 *         this.setActive( this.reallyActive );
 *         // To update the menu label
 *         this.toolbar.emit( 'updateState' );
 *     };
 *     SettingsTool.prototype.onUpdateState = function () {};
 *     toolFactory.register( SettingsTool );
 *
 *     // Register two more tools, nothing interesting here
 *     function StuffTool() {
 *         StuffTool.super.apply( this, arguments );
 *         this.reallyActive = false;
 *     }
 *     OO.inheritClass( StuffTool, OO.ui.Tool );
 *     StuffTool.static.name = 'stuff';
 *     StuffTool.static.icon = 'ellipsis';
 *     StuffTool.static.title = 'More stuff';
 *     StuffTool.prototype.onSelect = function () {
 *         $area.text( 'More stuff tool clicked!' );
 *         // Toggle the active state on each click
 *         this.reallyActive = !this.reallyActive;
 *         this.setActive( this.reallyActive );
 *         // To update the menu label
 *         this.toolbar.emit( 'updateState' );
 *     };
 *     StuffTool.prototype.onUpdateState = function () {};
 *     toolFactory.register( StuffTool );
 *
 *     // This is a PopupTool. Rather than having a custom 'onSelect' action, it will display a
 *     // little popup window (a PopupWidget). 'onUpdateState' is also already implemented.
 *     function HelpTool( toolGroup, config ) {
 *         OO.ui.PopupTool.call( this, toolGroup, $.extend( { popup: {
 *             padded: true,
 *             label: 'Help',
 *             head: true
 *         } }, config ) );
 *         this.popup.$body.append( '<p>I am helpful!</p>' );
 *     }
 *     OO.inheritClass( HelpTool, OO.ui.PopupTool );
 *     HelpTool.static.name = 'help';
 *     HelpTool.static.icon = 'help';
 *     HelpTool.static.title = 'Help';
 *     toolFactory.register( HelpTool );
 *
 *     // Finally define which tools and in what order appear in the toolbar. Each tool may only be
 *     // used once (but not all defined tools must be used).
 *     toolbar.setup( [
 *         {
 *             // 'bar' tool groups display tools' icons only, side-by-side.
 *             type: 'bar',
 *             include: [ 'search', 'help' ]
 *         },
 *         {
 *             // 'menu' tool groups display both the titles and icons, in a dropdown menu.
 *             // Menu label indicates which items are selected.
 *             type: 'menu',
 *             indicator: 'down',
 *             include: [ 'settings', 'stuff' ]
 *         }
 *     ] );
 *
 *     // Create some UI around the toolbar and place it in the document
 *     const frame = new OO.ui.PanelLayout( {
 *         expanded: false,
 *         framed: true
 *     } );
 *     const contentFrame = new OO.ui.PanelLayout( {
 *         expanded: false,
 *         padded: true
 *     } );
 *     frame.$element.append(
 *         toolbar.$element,
 *         contentFrame.$element.append( $area )
 *     );
 *     $( document.body ).append( frame.$element );
 *
 *     // Here is where the toolbar is actually built. This must be done after inserting it into the
 *     // document.
 *     toolbar.initialize();
 *     toolbar.emit( 'updateState' );
 *
 * @class
 * @extends OO.ui.Element
 * @mixes OO.EventEmitter
 * @mixes OO.ui.mixin.GroupElement
 *
 * @constructor
 * @param {OO.ui.ToolFactory} toolFactory Factory for creating tools
 * @param {OO.ui.ToolGroupFactory} toolGroupFactory Factory for creating toolgroups
 * @param {Object} [config] Configuration options
 * @param {boolean} [config.actions] Add an actions section to the toolbar. Actions are commands that are
 *  included in the toolbar, but are not configured as tools. By default, actions are displayed on
 *  the right side of the toolbar.
 *  This feature is deprecated. It is suggested to use the ToolGroup 'align' property instead.
 * @param {string} [config.position='top'] Whether the toolbar is positioned above ('top') or below
 *  ('bottom') content.
 * @param {jQuery} [config.$overlay] An overlay for the popup.
 *  See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
 */
OO.ui.Toolbar = function OoUiToolbar( toolFactory, toolGroupFactory, config ) {
	// Allow passing positional parameters inside the config object
	if ( OO.isPlainObject( toolFactory ) && config === undefined ) {
		config = toolFactory;
		toolFactory = config.toolFactory;
		toolGroupFactory = config.toolGroupFactory;
	}

	// Configuration initialization
	config = config || {};

	// Parent constructor
	OO.ui.Toolbar.super.call( this, config );

	// Mixin constructors
	OO.EventEmitter.call( this );
	OO.ui.mixin.GroupElement.call( this, config );

	// Properties
	this.toolFactory = toolFactory;
	this.toolGroupFactory = toolGroupFactory;
	this.groupsByName = {};
	this.activeToolGroups = 0;
	this.tools = {};
	this.position = config.position || 'top';
	this.$bar = $( '<div>' );
	this.$after = $( '<div>' );
	this.$actions = $( '<div>' );
	this.$popups = $( '<div>' );
	this.initialized = false;
	this.narrow = false;
	this.narrowThreshold = null;
	this.onWindowResizeHandler = this.onWindowResize.bind( this );
	this.$overlay = ( config.$overlay === true ? OO.ui.getDefaultOverlay() : config.$overlay ) ||
		this.$element;

	// Events
	this.$element
		.add( this.$bar ).add( this.$group ).add( this.$after ).add( this.$actions )
		.on( 'mousedown keydown', this.onPointerDown.bind( this ) );

	// Initialization
	this.$bar.addClass( 'oo-ui-toolbar-bar' );
	this.$group.addClass( 'oo-ui-toolbar-tools' );
	this.$after.addClass( 'oo-ui-toolbar-tools oo-ui-toolbar-after' );
	this.$popups.addClass( 'oo-ui-toolbar-popups' );

	this.$bar.append( this.$group, this.$after );
	if ( config.actions ) {
		this.$bar.append( this.$actions.addClass( 'oo-ui-toolbar-actions' ) );
	}
	this.$bar.append( $( '<div>' ).css( 'clear', 'both' ) );

	// Possible classes: oo-ui-toolbar-position-top, oo-ui-toolbar-position-bottom
	this.$element
		.addClass( 'oo-ui-toolbar oo-ui-toolbar-position-' + this.position )
		.append( this.$bar );
	this.$overlay.append( this.$popups );
};

/* Setup */

OO.inheritClass( OO.ui.Toolbar, OO.ui.Element );
OO.mixinClass( OO.ui.Toolbar, OO.EventEmitter );
OO.mixinClass( OO.ui.Toolbar, OO.ui.mixin.GroupElement );

/* Events */

/**
 * An 'updateState' event must be emitted on the Toolbar (by calling
 * `toolbar.emit( 'updateState' )`) every time the state of the application using the toolbar
 * changes, and an update to the state of tools is required.
 *
 * @event OO.ui.Toolbar#updateState
 * @param {...any} data Application-defined parameters
 */

/**
 * An 'active' event is emitted when the number of active toolgroups increases from 0, or
 * returns to 0.
 *
 * @event OO.ui.Toolbar#active
 * @param {boolean} There are active toolgroups in this toolbar
 */

/**
 * Toolbar has resized to a point where narrow mode has changed
 *
 * @event OO.ui.Toolbar#resize
 */

/* Methods */

/**
 * Get the tool factory.
 *
 * @return {OO.ui.ToolFactory} Tool factory
 */
OO.ui.Toolbar.prototype.getToolFactory = function () {
	return this.toolFactory;
};

/**
 * Get the toolgroup factory.
 *
 * @return {OO.Factory} Toolgroup factory
 */
OO.ui.Toolbar.prototype.getToolGroupFactory = function () {
	return this.toolGroupFactory;
};

/**
 * @inheritdoc {OO.ui.mixin.GroupElement}
 */
OO.ui.Toolbar.prototype.insertItemElements = function ( item ) {
	// Mixin method
	OO.ui.mixin.GroupElement.prototype.insertItemElements.apply( this, arguments );

	if ( item.align === 'after' ) {
		// Toolbar only ever appends ToolGroups to the end, so we can ignore 'index'
		this.$after.append( item.$element );
	}
};

/**
 * Handles mouse down events.
 *
 * @private
 * @param {jQuery.Event} e Mouse down event
 * @return {undefined|boolean} False to prevent default if event is handled
 */
OO.ui.Toolbar.prototype.onPointerDown = function ( e ) {
	const $closestWidgetToEvent = $( e.target ).closest( '.oo-ui-widget' ),
		$closestWidgetToToolbar = this.$element.closest( '.oo-ui-widget' );
	if (
		!$closestWidgetToEvent.length ||
		$closestWidgetToEvent[ 0 ] ===
		$closestWidgetToToolbar[ 0 ]
	) {
		return false;
	}
};

/**
 * Handle window resize event.
 *
 * @private
 * @param {jQuery.Event} e Window resize event
 */
OO.ui.Toolbar.prototype.onWindowResize = function () {
	this.setNarrow( this.$bar[ 0 ].clientWidth <= this.getNarrowThreshold() );
};

/**
 * Check if the toolbar is in narrow mode
 *
 * @return {boolean} Toolbar is in narrow mode
 */
OO.ui.Toolbar.prototype.isNarrow = function () {
	return this.narrow;
};

/**
 * Set the narrow mode flag
 *
 * @param {boolean} narrow Toolbar is in narrow mode
 */
OO.ui.Toolbar.prototype.setNarrow = function ( narrow ) {
	if ( narrow !== this.narrow ) {
		this.narrow = narrow;
		this.$element.add( this.$popups ).toggleClass(
			'oo-ui-toolbar-narrow',
			this.narrow
		);
		this.emit( 'resize' );
	}
};

/**
 * Get the (lazily-computed) width threshold for applying the oo-ui-toolbar-narrow
 * class.
 *
 * @private
 * @return {number} Width threshold in pixels
 */
OO.ui.Toolbar.prototype.getNarrowThreshold = function () {
	if ( this.narrowThreshold === null ) {
		this.narrowThreshold = this.$group[ 0 ].offsetWidth + this.$after[ 0 ].offsetWidth +
			this.$actions[ 0 ].offsetWidth;
	}
	return this.narrowThreshold;
};

/**
 * Sets up handles and preloads required information for the toolbar to work.
 * This must be called after it is attached to a visible document and before doing anything else.
 */
OO.ui.Toolbar.prototype.initialize = function () {
	if ( !this.initialized ) {
		this.initialized = true;
		$( this.getElementWindow() ).on( 'resize', this.onWindowResizeHandler );
		this.onWindowResize();
	}
};

/**
 * Set up the toolbar.
 *
 * The toolbar is set up with a list of toolgroup configurations that specify the type of
 * toolgroup ({@link OO.ui.BarToolGroup bar}, {@link OO.ui.MenuToolGroup menu}, or
 * {@link OO.ui.ListToolGroup list}) to add and which tools to include, exclude, promote, or demote
 * within that toolgroup. Please see {@link OO.ui.ToolGroup toolgroups} for more information about
 * including tools in toolgroups.
 *
 * @param {Object[]} groups List of toolgroup configurations
 * @param {string} groups.name Symbolic name for this toolgroup
 * @param {string} [groups.type] Toolgroup type, e.g. "bar", "list", or "menu". Should exist in the
 *  {@link OO.ui.ToolGroupFactory} provided via the constructor. Defaults to "list" for catch-all
 *  groups where `include='*'`, otherwise "bar".
 * @param {Array|string} [groups.include] Tools to include in the toolgroup, or "*" for catch-all,
 *  see {@link OO.ui.ToolFactory#extract}
 * @param {Array|string} [groups.exclude] Tools to exclude from the toolgroup
 * @param {Array|string} [groups.promote] Tools to promote to the beginning of the toolgroup
 * @param {Array|string} [groups.demote] Tools to demote to the end of the toolgroup
 */
OO.ui.Toolbar.prototype.setup = function ( groups ) {
	const defaultType = 'bar';

	// Cleanup previous groups
	this.reset();

	const items = [];
	// Build out new groups
	for ( let i = 0, len = groups.length; i < len; i++ ) {
		const groupConfig = groups[ i ];
		if ( groupConfig.include === '*' ) {
			// Apply defaults to catch-all groups
			if ( groupConfig.type === undefined ) {
				groupConfig.type = 'list';
			}
			if ( groupConfig.label === undefined ) {
				groupConfig.label = OO.ui.msg( 'ooui-toolbar-more' );
			}
		}
		// Check type has been registered
		const type = this.getToolGroupFactory().lookup( groupConfig.type ) ?
			groupConfig.type : defaultType;
		const toolGroup = this.getToolGroupFactory().create( type, this, groupConfig );
		items.push( toolGroup );
		this.groupsByName[ groupConfig.name ] = toolGroup;
		toolGroup.connect( this, {
			active: 'onToolGroupActive'
		} );
	}
	this.addItems( items );
};

/**
 * Handle active events from tool groups
 *
 * @param {boolean} active Tool group has become active, inactive if false
 * @fires OO.ui.Toolbar#active
 */
OO.ui.Toolbar.prototype.onToolGroupActive = function ( active ) {
	if ( active ) {
		this.activeToolGroups++;
		if ( this.activeToolGroups === 1 ) {
			this.emit( 'active', true );
		}
	} else {
		this.activeToolGroups--;
		if ( this.activeToolGroups === 0 ) {
			this.emit( 'active', false );
		}
	}
};

/**
 * Get a toolgroup by name
 *
 * @param {string} name Group name
 * @return {OO.ui.ToolGroup|null} Tool group, or null if none found by that name
 */
OO.ui.Toolbar.prototype.getToolGroupByName = function ( name ) {
	return this.groupsByName[ name ] || null;
};

/**
 * Remove all tools and toolgroups from the toolbar.
 */
OO.ui.Toolbar.prototype.reset = function () {
	this.groupsByName = {};
	this.tools = {};
	for ( let i = 0, len = this.items.length; i < len; i++ ) {
		this.items[ i ].destroy();
	}
	this.clearItems();
};

/**
 * Destroy the toolbar.
 *
 * Destroying the toolbar removes all event handlers and DOM elements that constitute the toolbar.
 * Call this method whenever you are done using a toolbar.
 */
OO.ui.Toolbar.prototype.destroy = function () {
	$( this.getElementWindow() ).off( 'resize', this.onWindowResizeHandler );
	this.reset();
	this.$element.remove();
};

/**
 * Check if the tool is available.
 *
 * Available tools are ones that have not yet been added to the toolbar.
 *
 * @param {string} name Symbolic name of tool
 * @return {boolean} Tool is available
 */
OO.ui.Toolbar.prototype.isToolAvailable = function ( name ) {
	return !this.tools[ name ];
};

/**
 * Prevent tool from being used again.
 *
 * @param {OO.ui.Tool} tool Tool to reserve
 */
OO.ui.Toolbar.prototype.reserveTool = function ( tool ) {
	this.tools[ tool.getName() ] = tool;
};

/**
 * Allow tool to be used again.
 *
 * @param {OO.ui.Tool} tool Tool to release
 */
OO.ui.Toolbar.prototype.releaseTool = function ( tool ) {
	delete this.tools[ tool.getName() ];
};

/**
 * Get accelerator label for tool.
 *
 * The OOUI library does not contain an accelerator system, but this is the hook for one. To
 * use an accelerator system, subclass the toolbar and override this method, which is meant to
 * return a label that describes the accelerator keys for the tool passed (by symbolic name) to
 * the method.
 *
 * @param {string} name Symbolic name of tool
 * @return {string|undefined} Tool accelerator label if available
 */
OO.ui.Toolbar.prototype.getToolAccelerator = function () {
	return undefined;
};