/**
 * IndexLayouts contain {@link OO.ui.TabPanelLayout tab panel layouts} as well as
 * {@link OO.ui.TabSelectWidget tabs} that allow users to easily navigate through the tab panels
 * and select which one to display. By default, only one tab panel is displayed at a time. When a
 * user navigates to a new tab panel, the index layout automatically focuses on the first focusable
 * element, unless the default setting is changed.
 *
 * TODO: This class is similar to BookletLayout, we may want to refactor to reduce duplication
 *
 *     @example
 *     // Example of a IndexLayout that contains two TabPanelLayouts.
 *
 *     function TabPanelOneLayout( name, config ) {
 *         TabPanelOneLayout.super.call( this, name, config );
 *         this.$element.append( '<p>First tab panel</p>' );
 *     }
 *     OO.inheritClass( TabPanelOneLayout, OO.ui.TabPanelLayout );
 *     TabPanelOneLayout.prototype.setupTabItem = function () {
 *         this.tabItem.setLabel( 'Tab panel one' );
 *     };
 *
 *     const tabPanel1 = new TabPanelOneLayout( 'one' ),
 *         tabPanel2 = new OO.ui.TabPanelLayout( 'two', { label: 'Tab panel two' } );
 *
 *     tabPanel2.$element.append( '<p>Second tab panel</p>' );
 *
 *     const index = new OO.ui.IndexLayout();
 *
 *     index.addTabPanels( [ tabPanel1, tabPanel2 ] );
 *     $( document.body ).append( index.$element );
 *
 * @class
 * @extends OO.ui.MenuLayout
 *
 * @constructor
 * @param {Object} [config] Configuration options
 * @param {OO.ui.StackLayout} [config.contentPanel] Content stack (see MenuLayout)
 * @param {boolean} [config.continuous=false] Show all tab panels, one after another
 * @param {boolean} [config.autoFocus=true] Focus on the first focusable element when a new tab panel is
 *  displayed. Disabled on mobile.
 * @param {boolean} [config.framed=true] Render the tabs with frames
 * @param {boolean} [config.openMatchedPanels=true] Automatically switch to a panel when the browser's
 *  find-in-page feature matches content there, in browsers that support it.
 */
OO.ui.IndexLayout = function OoUiIndexLayout( config ) {
	// Configuration initialization
	config = Object.assign( {}, config, { menuPosition: 'top' } );

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

	// Properties
	this.currentTabPanelName = null;
	// Allow infused widgets to pass existing tabPanels
	this.tabPanels = config.tabPanels || {};
	this.openMatchedPanels = config.openMatchedPanels === undefined || !!config.openMatchedPanels;

	this.ignoreFocus = false;
	if ( this.contentPanel ) {
		this.contentPanel.setHideUntilFound( this.openMatchedPanels );
	}
	this.stackLayout = this.contentPanel || new OO.ui.StackLayout( {
		continuous: !!config.continuous,
		expanded: this.expanded,
		hideUntilFound: this.openMatchedPanels
	} );
	this.setContentPanel( this.stackLayout );
	this.autoFocus = config.autoFocus === undefined || !!config.autoFocus;

	if ( config.tabSelectWidget ) {
		// If we are using a custom tabSelectWidget (e.g. infusing) then
		// ensure the tabPanels are linked to tabItems
		this.stackLayout.getItems().forEach( ( tabPanel, i ) => {
			if ( !tabPanel.getTabItem() ) {
				tabPanel.setTabItem( config.tabSelectWidget.items[ i ] || null );
			}
		} );
	}
	// Allow infused widgets to pass an existing tabSelectWidget
	this.tabSelectWidget = config.tabSelectWidget || new OO.ui.TabSelectWidget( {
		framed: config.framed === undefined || config.framed
	} );
	this.tabPanel = this.menuPanel || new OO.ui.PanelLayout( {
		expanded: this.expanded
	} );
	this.setMenuPanel( this.tabPanel );

	this.toggleMenu( true );

	// Events
	this.stackLayout.connect( this, {
		set: 'onStackLayoutSet'
	} );
	if ( this.openMatchedPanels ) {
		this.stackLayout.$element.on( 'beforematch', this.onStackLayoutBeforeMatch.bind( this ) );
	}
	this.tabSelectWidget.connect( this, {
		select: 'onTabSelectWidgetSelect'
	} );
	if ( this.autoFocus ) {
		// Event 'focus' does not bubble, but 'focusin' does.
		this.stackLayout.$element.on( 'focusin', this.onStackLayoutFocus.bind( this ) );
	}

	// Initialization
	this.$element.addClass( 'oo-ui-indexLayout' );
	this.stackLayout.$element.addClass( 'oo-ui-indexLayout-stackLayout' );
	this.tabPanel.$element
		.addClass( 'oo-ui-indexLayout-tabPanel' )
		.append( this.tabSelectWidget.$element );

	this.selectFirstSelectableTabPanel();
};

/* Setup */

OO.inheritClass( OO.ui.IndexLayout, OO.ui.MenuLayout );

/* Events */

/**
 * A 'set' event is emitted when a tab panel is {@link OO.ui.IndexLayout#setTabPanel set} to be displayed by the
 * index layout.
 *
 * @event OO.ui.IndexLayout#set
 * @param {OO.ui.TabPanelLayout} tabPanel Current tab panel
 */

/**
 * An 'add' event is emitted when tab panels are {@link OO.ui.IndexLayout#addTabPanels added} to the index layout.
 *
 * @event OO.ui.IndexLayout#add
 * @param {OO.ui.TabPanelLayout[]} tabPanel Added tab panels
 * @param {number} index Index tab panels were added at
 */

/**
 * A 'remove' event is emitted when tab panels are {@link OO.ui.IndexLayout#clearTabPanels cleared} or
 * {@link OO.ui.IndexLayout#removeTabPanels removed} from the index.
 *
 * @event OO.ui.IndexLayout#remove
 * @param {OO.ui.TabPanelLayout[]} tabPanel Removed tab panels
 */

/* Methods */

/**
 * Handle stack layout focus.
 *
 * @private
 * @param {jQuery.Event} e Focusing event
 */
OO.ui.IndexLayout.prototype.onStackLayoutFocus = function ( e ) {
	// Find the tab panel that an element was focused within
	const $target = $( e.target ).closest( '.oo-ui-tabPanelLayout' );
	for ( const name in this.tabPanels ) {
		// Check for tab panel match, exclude current tab panel to find only tab panel changes
		if ( this.tabPanels[ name ].$element[ 0 ] === $target[ 0 ] &&
				name !== this.currentTabPanelName ) {
			this.setTabPanel( name );
			break;
		}
	}
};

/**
 * Handle stack layout set events.
 *
 * @private
 * @param {OO.ui.PanelLayout|null} tabPanel The tab panel that is now the current panel
 */
OO.ui.IndexLayout.prototype.onStackLayoutSet = function ( tabPanel ) {
	// If everything is unselected, do nothing
	if ( !tabPanel ) {
		return;
	}
	// Focus the first element on the newly selected panel
	if ( this.autoFocus && !OO.ui.isMobile() ) {
		this.focus();
	}
};

/**
 * Handle beforematch events triggered by the browser's find-in-page feature
 *
 * @param {Event} event 'beforematch' event
 */
OO.ui.IndexLayout.prototype.onStackLayoutBeforeMatch = function ( event ) {
	let tabPanel;
	// Find TabPanel from DOM node
	this.stackLayout.getItems().some( ( item ) => {
		if ( item.$element[ 0 ] === event.target ) {
			tabPanel = item;
			return true;
		}
		return false;
	} );
	if ( tabPanel ) {
		const tabItem = tabPanel.getTabItem();
		if ( tabItem ) {
			this.tabSelectWidget.selectItem( tabItem );
		}
	}
};

/**
 * Focus the first input in the current tab panel.
 *
 * If no tab panel is selected, the first selectable tab panel will be selected.
 * If the focus is already in an element on the current tab panel, nothing will happen.
 *
 * @param {number} [itemIndex] A specific item to focus on
 */
OO.ui.IndexLayout.prototype.focus = function ( itemIndex ) {
	const items = this.stackLayout.getItems();

	let tabPanel;
	if ( itemIndex !== undefined && items[ itemIndex ] ) {
		tabPanel = items[ itemIndex ];
	} else {
		tabPanel = this.stackLayout.getCurrentItem();
	}

	if ( !tabPanel ) {
		this.selectFirstSelectableTabPanel();
		tabPanel = this.stackLayout.getCurrentItem();
	}
	if ( !tabPanel ) {
		return;
	}
	// Only change the focus if is not already in the current page
	if ( !OO.ui.contains(
		tabPanel.$element[ 0 ],
		this.getElementDocument().activeElement,
		true
	) ) {
		tabPanel.focus();
	}
};

/**
 * Find the first focusable input in the index layout and focus
 * on it.
 */
OO.ui.IndexLayout.prototype.focusFirstFocusable = function () {
	OO.ui.findFocusable( this.stackLayout.$element ).focus();
};

/**
 * Handle tab widget select events.
 *
 * @private
 * @param {OO.ui.OptionWidget|null} item Selected item
 */
OO.ui.IndexLayout.prototype.onTabSelectWidgetSelect = function ( item ) {
	if ( item ) {
		this.setTabPanel( item.getData() );
	}
};

/**
 * Get the tab panel closest to the specified tab panel.
 *
 * @param {OO.ui.TabPanelLayout} tabPanel Tab panel to use as a reference point
 * @return {OO.ui.TabPanelLayout|null} Tab panel closest to the specified
 */
OO.ui.IndexLayout.prototype.getClosestTabPanel = function ( tabPanel ) {
	const tabPanels = this.stackLayout.getItems(),
		index = tabPanels.indexOf( tabPanel );

	if ( index === -1 ) {
		return null;
	}

	const next = tabPanels[ index + 1 ];
	const prev = tabPanels[ index - 1 ];
	// Prefer adjacent tab panels at the same level
	const level = this.tabSelectWidget.findItemFromData( tabPanel.getName() ).getLevel();
	if (
		prev &&
		level === this.tabSelectWidget.findItemFromData( prev.getName() ).getLevel()
	) {
		return prev;
	}
	if (
		next &&
		level === this.tabSelectWidget.findItemFromData( next.getName() ).getLevel()
	) {
		return next;
	}
	return prev || next || null;
};

/**
 * Get the tabs widget.
 *
 * @return {OO.ui.TabSelectWidget} Tabs widget
 */
OO.ui.IndexLayout.prototype.getTabs = function () {
	return this.tabSelectWidget;
};

/**
 * Get a tab panel by its symbolic name.
 *
 * @param {string} name Symbolic name of tab panel
 * @return {OO.ui.TabPanelLayout|undefined} Tab panel, if found
 */
OO.ui.IndexLayout.prototype.getTabPanel = function ( name ) {
	return this.tabPanels[ name ];
};

/**
 * Get the current tab panel.
 *
 * @return {OO.ui.TabPanelLayout|undefined} Current tab panel, if found
 */
OO.ui.IndexLayout.prototype.getCurrentTabPanel = function () {
	const name = this.getCurrentTabPanelName();
	return name ? this.getTabPanel( name ) : undefined;
};

/**
 * Get the symbolic name of the current tab panel.
 *
 * @return {string|null} Symbolic name of the current tab panel
 */
OO.ui.IndexLayout.prototype.getCurrentTabPanelName = function () {
	return this.currentTabPanelName;
};

/**
 * Add tab panels to the index layout.
 *
 * When tab panels are added with the same names as existing tab panels, the existing tab panels
 * will be automatically removed before the new tab panels are added.
 *
 * @param {OO.ui.TabPanelLayout[]} tabPanels Tab panels to add
 * @param {number} index Index of the insertion point
 * @fires OO.ui.IndexLayout#add
 * @chainable
 * @return {OO.ui.IndexLayout} The layout, for chaining
 */
OO.ui.IndexLayout.prototype.addTabPanels = function ( tabPanels, index ) {
	let i, len, name, tabPanel, tabItem, currentIndex;
	const stackLayoutTabPanels = this.stackLayout.getItems(),
		remove = [],
		tabItems = [];

	// Remove tab panels with same names
	for ( i = 0, len = tabPanels.length; i < len; i++ ) {
		tabPanel = tabPanels[ i ];
		name = tabPanel.getName();

		if ( Object.prototype.hasOwnProperty.call( this.tabPanels, name ) ) {
			// Correct the insertion index
			currentIndex = stackLayoutTabPanels.indexOf( this.tabPanels[ name ] );
			if ( currentIndex !== -1 && currentIndex + 1 < index ) {
				index--;
			}
			remove.push( this.tabPanels[ name ] );
		}
	}
	if ( remove.length ) {
		this.removeTabPanels( remove );
	}

	// Add new tab panels
	for ( i = 0, len = tabPanels.length; i < len; i++ ) {
		tabPanel = tabPanels[ i ];
		name = tabPanel.getName();
		this.tabPanels[ name ] = tabPanel;
		tabItem = new OO.ui.TabOptionWidget(
			Object.assign( { data: name }, tabPanel.getTabItemConfig() )
		);
		tabPanel.setTabItem( tabItem );
		tabItems.push( tabItem );
	}

	if ( tabItems.length ) {
		this.tabSelectWidget.addItems( tabItems, index );
		this.selectFirstSelectableTabPanel();
	}
	this.stackLayout.addItems( tabPanels, index );
	this.emit( 'add', tabPanels, index );

	return this;
};

/**
 * Remove the specified tab panels from the index layout.
 *
 * To remove all tab panels from the index, you may wish to use the #clearTabPanels method instead.
 *
 * @param {OO.ui.TabPanelLayout[]} tabPanels An array of tab panels to remove
 * @fires OO.ui.IndexLayout#remove
 * @chainable
 * @return {OO.ui.IndexLayout} The layout, for chaining
 */
OO.ui.IndexLayout.prototype.removeTabPanels = function ( tabPanels ) {
	let i, len, name, tabPanel;

	const items = [];

	for ( i = 0, len = tabPanels.length; i < len; i++ ) {
		tabPanel = tabPanels[ i ];
		name = tabPanel.getName();
		delete this.tabPanels[ name ];
		items.push( this.tabSelectWidget.findItemFromData( name ) );
		tabPanel.setTabItem( null );
	}
	if ( items.length ) {
		this.tabSelectWidget.removeItems( items );
		this.selectFirstSelectableTabPanel();
	}
	this.stackLayout.removeItems( tabPanels );
	this.emit( 'remove', tabPanels );

	return this;
};

/**
 * Clear all tab panels from the index layout.
 *
 * To remove only a subset of tab panels from the index, use the #removeTabPanels method.
 *
 * @fires OO.ui.IndexLayout#remove
 * @chainable
 * @return {OO.ui.IndexLayout} The layout, for chaining
 */
OO.ui.IndexLayout.prototype.clearTabPanels = function () {
	let i, len;

	const tabPanels = this.stackLayout.getItems();

	this.tabPanels = {};
	this.currentTabPanelName = null;
	this.tabSelectWidget.clearItems();
	for ( i = 0, len = tabPanels.length; i < len; i++ ) {
		tabPanels[ i ].setTabItem( null );
	}
	this.stackLayout.clearItems();

	this.emit( 'remove', tabPanels );

	return this;
};

/**
 * Set the current tab panel by symbolic name.
 *
 * @fires OO.ui.IndexLayout#set
 * @param {string} name Symbolic name of tab panel
 */
OO.ui.IndexLayout.prototype.setTabPanel = function ( name ) {
	let selectedItem,
		$focused,
		previousTabPanel,
		tabPanel;

	if ( name !== this.currentTabPanelName ) {
		tabPanel = this.getTabPanel( name );
		previousTabPanel = this.getCurrentTabPanel();
		selectedItem = this.tabSelectWidget.findSelectedItem();
		if ( !selectedItem || selectedItem.getData() !== name ) {
			this.tabSelectWidget.selectItemByData( name );
		}
		if ( tabPanel ) {
			if ( previousTabPanel ) {
				previousTabPanel.setActive( false );
				// Blur anything focused if the next tab panel doesn't have anything focusable.
				// This is not needed if the next tab panel has something focusable (because once
				// it is focused this blur happens automatically). If the layout is non-continuous,
				// this check is meaningless because the next tab panel is not visible yet and thus
				// can't hold focus.
				if (
					this.autoFocus &&
					!OO.ui.isMobile() &&
					this.stackLayout.isContinuous() &&
					OO.ui.findFocusable( tabPanel.$element ).length !== 0
				) {
					$focused = previousTabPanel.$element.find( ':focus' );
					if ( $focused.length ) {
						$focused[ 0 ].blur();
					}
				}
			}
			this.currentTabPanelName = name;
			tabPanel.setActive( true );
			this.stackLayout.setItem( tabPanel );
			if ( !this.stackLayout.isContinuous() && previousTabPanel ) {
				// This should not be necessary, since any inputs on the previous tab panel should
				// have been blurred when it was hidden, but browsers are not very consistent about
				// this.
				$focused = previousTabPanel.$element.find( ':focus' );
				if ( $focused.length ) {
					$focused[ 0 ].blur();
				}
			}
			this.emit( 'set', tabPanel );
		}
	}
};

/**
 * Select the first selectable tab panel.
 *
 * @chainable
 * @return {OO.ui.IndexLayout} The layout, for chaining
 */
OO.ui.IndexLayout.prototype.selectFirstSelectableTabPanel = function () {
	if ( !this.tabSelectWidget.findSelectedItem() ) {
		this.tabSelectWidget.selectItem( this.tabSelectWidget.findFirstSelectableItem() );
	}

	return this;
};