/**
 * StackLayouts contain a series of {@link OO.ui.PanelLayout panel layouts}. By default, only one
 * panel is displayed at a time, though the stack layout can also be configured to show all
 * contained panels, one after another, by setting the #continuous option to 'true'.
 *
 *     @example
 *     // A stack layout with two panels, configured to be displayed continuously
 *     const myStack = new OO.ui.StackLayout( {
 *         items: [
 *             new OO.ui.PanelLayout( {
 *                 $content: $( '<p>Panel One</p>' ),
 *                 padded: true,
 *                 framed: true
 *             } ),
 *             new OO.ui.PanelLayout( {
 *                 $content: $( '<p>Panel Two</p>' ),
 *                 padded: true,
 *                 framed: true
 *             } )
 *         ],
 *         continuous: true
 *     } );
 *     $( document.body ).append( myStack.$element );
 *
 * @class
 * @extends OO.ui.PanelLayout
 * @mixes OO.ui.mixin.GroupElement
 *
 * @constructor
 * @param {Object} [config] Configuration options
 * @param {boolean} [config.continuous=false] Show all panels, one after another. By default, only one panel
 *  is displayed at a time.
 * @param {OO.ui.Layout[]} [config.items] Panel layouts to add to the stack layout.
 * @param {boolean} [config.hideUntilFound] Hide panels using hidden="until-found", meaning they will be
 *  shown when matched with the browser's find-and-replace feature if supported.
 */
OO.ui.StackLayout = function OoUiStackLayout( config ) {
	// Configuration initialization
	// Make the layout scrollable in continuous mode, otherwise each
	// panel is responsible for its own scrolling.
	config = Object.assign( { scrollable: !!( config && config.continuous ) }, config );

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

	// Mixin constructors
	OO.ui.mixin.GroupElement.call( this, Object.assign( { $group: this.$element }, config ) );

	// Properties
	this.currentItem = null;
	this.setContinuous( !!config.continuous );
	this.hideUntilFound = !!config.hideUntilFound;

	// Initialization
	this.$element.addClass( 'oo-ui-stackLayout' );
	this.addItems( config.items || [] );
};

/* Setup */

OO.inheritClass( OO.ui.StackLayout, OO.ui.PanelLayout );
OO.mixinClass( OO.ui.StackLayout, OO.ui.mixin.GroupElement );

/* Events */

/**
 * A 'set' event is emitted when panels are {@link OO.ui.StackLayout#addItems added}, {@link OO.ui.StackLayout#removeItems removed},
 * {@link OO.ui.StackLayout#clearItems cleared} or {@link OO.ui.StackLayout#setItem displayed}.
 *
 * @event OO.ui.StackLayout#set
 * @param {OO.ui.Layout|null} item Current panel or `null` if no panel is shown
 */

/* Methods */

/**
 * Set the layout to continuous mode or not
 *
 * @param {boolean} continuous Continuous mode
 */
OO.ui.StackLayout.prototype.setContinuous = function ( continuous ) {
	this.continuous = continuous;
	this.$element.toggleClass( 'oo-ui-stackLayout-continuous', !!continuous );
	// Force an update of the attributes used to hide/show items
	this.updateHiddenState( this.items, this.currentItem );
};

/**
 * Check if the layout is in continuous mode
 *
 * @return {boolean} The layout is in continuous mode
 */
OO.ui.StackLayout.prototype.isContinuous = function () {
	return this.continuous;
};

/**
 * Get the current panel.
 *
 * @return {OO.ui.Layout|null}
 */
OO.ui.StackLayout.prototype.getCurrentItem = function () {
	return this.currentItem;
};

/**
 * Unset the current item.
 *
 * @private
 * @param {OO.ui.StackLayout} layout
 * @fires OO.ui.StackLayout#set
 */
OO.ui.StackLayout.prototype.unsetCurrentItem = function () {
	const prevItem = this.currentItem;
	if ( prevItem === null ) {
		return;
	}

	this.currentItem = null;
	this.emit( 'set', null );
};

/**
 * Set the hideUntilFound config (see contructor)
 *
 * @param {boolean} hideUntilFound
 */
OO.ui.StackLayout.prototype.setHideUntilFound = function ( hideUntilFound ) {
	this.hideUntilFound = hideUntilFound;
	// Force an update of the attributes used to hide/show items
	this.updateHiddenState( this.items, this.currentItem );
};

/**
 * Add panel layouts to the stack layout.
 *
 * Panels will be added to the end of the stack layout array unless the optional index parameter
 * specifies a different insertion point. Adding a panel that is already in the stack will move it
 * to the end of the array or the point specified by the index.
 *
 * @param {OO.ui.Layout[]} [items] Panels to add
 * @param {number} [index] Index of the insertion point
 * @chainable
 * @return {OO.ui.StackLayout} The layout, for chaining
 */
OO.ui.StackLayout.prototype.addItems = function ( items, index ) {
	if ( !items || items.length === 0 ) {
		return this;
	}

	// Update the visibility
	this.updateHiddenState( items, this.currentItem );

	// Mixin method
	OO.ui.mixin.GroupElement.prototype.addItems.call( this, items, index );

	if ( !this.currentItem ) {
		this.setItem( items[ 0 ] );
	}

	return this;
};

/**
 * Remove the specified panels from the stack layout.
 *
 * Removed panels are detached from the DOM, not removed, so that they may be reused. To remove all
 * panels, you may wish to use the #clearItems method instead.
 *
 * @param {OO.ui.Layout[]} itemsToRemove Panels to remove
 * @chainable
 * @return {OO.ui.StackLayout} The layout, for chaining
 * @fires OO.ui.StackLayout#set
 */
OO.ui.StackLayout.prototype.removeItems = function ( itemsToRemove ) {
	const isCurrentItemRemoved = itemsToRemove.indexOf( this.currentItem ) !== -1;

	let nextItem;
	if ( isCurrentItemRemoved ) {
		let i = this.items.indexOf( this.currentItem );
		do {
			nextItem = this.items[ ++i ];
		} while ( nextItem && itemsToRemove.indexOf( nextItem ) !== -1 );
	}

	// Mixin method
	OO.ui.mixin.GroupElement.prototype.removeItems.call( this, itemsToRemove );

	if ( isCurrentItemRemoved ) {
		if ( this.items.length ) {
			this.setItem( nextItem || this.items[ this.items.length - 1 ] );
		} else {
			this.unsetCurrentItem();
		}
	}

	return this;
};

/**
 * Clear all panels from the stack layout.
 *
 * Cleared panels are detached from the DOM, not removed, so that they may be reused. To remove only
 * a subset of panels, use the #removeItems method.
 *
 * @chainable
 * @return {OO.ui.StackLayout} The layout, for chaining
 * @fires OO.ui.StackLayout#set
 */
OO.ui.StackLayout.prototype.clearItems = function () {
	this.unsetCurrentItem();
	OO.ui.mixin.GroupElement.prototype.clearItems.call( this );

	return this;
};

/**
 * Show the specified panel.
 *
 * If another panel is currently displayed, it will be hidden.
 *
 * @param {OO.ui.Layout} item Panel to show
 * @chainable
 * @return {OO.ui.StackLayout} The layout, for chaining
 * @fires OO.ui.StackLayout#set
 */
OO.ui.StackLayout.prototype.setItem = function ( item ) {
	if ( item !== this.currentItem ) {
		this.updateHiddenState( this.items, item );

		if ( this.items.indexOf( item ) !== -1 ) {
			this.currentItem = item;
			this.emit( 'set', item );
		} else {
			this.unsetCurrentItem();
		}
	}

	return this;
};

/**
 * Reset the scroll offset of all panels, or the container if continuous
 *
 * @inheritdoc
 */
OO.ui.StackLayout.prototype.resetScroll = function () {
	if ( this.isContinuous() ) {
		// Parent method
		return OO.ui.StackLayout.super.prototype.resetScroll.call( this );
	}
	// Reset each panel
	this.getItems().forEach( ( panel ) => {
		// eslint-disable-next-line no-jquery/no-class-state
		const hidden = panel.$element.hasClass( 'oo-ui-element-hidden' );
		// Scroll can only be reset when panel is visible
		panel.$element.removeClass( 'oo-ui-element-hidden' );
		panel.resetScroll();
		if ( hidden ) {
			panel.$element.addClass( 'oo-ui-element-hidden' );
		}
	} );

	return this;
};

/**
 * Update the visibility of all items in case of non-continuous view.
 *
 * Ensure all items are hidden except for the selected one.
 * This method does nothing when the stack is continuous.
 *
 * @private
 * @param {OO.ui.Layout[]} items Item list iterate over
 * @param {OO.ui.Layout} [selectedItem] Selected item to show
 */
OO.ui.StackLayout.prototype.updateHiddenState = function ( items, selectedItem ) {
	if ( !this.isContinuous() ) {
		items.forEach( ( item ) => {
			if ( !selectedItem || selectedItem !== item ) {
				// If the panel is a TabPanelLayout which has a disabled tab, then
				// fully hide it so we don't search inside it and automatically switch
				// to it.
				const isDisabled = item instanceof OO.ui.TabPanelLayout &&
					item.getTabItem() && item.getTabItem().isDisabled();
				const hideUntilFound = !isDisabled && this.hideUntilFound;
				// jQuery "fixes" the value of the hidden attribute to always be "hidden"
				// Browsers which don't support 'until-found' will still hide the element
				item.$element[ 0 ].setAttribute( 'hidden', hideUntilFound ? 'until-found' : 'hidden' );
				item.$element.attr( 'aria-hidden', 'true' );
			}
		} );
		if ( selectedItem ) {
			selectedItem.$element[ 0 ].removeAttribute( 'hidden' );
			selectedItem.$element.removeAttr( 'aria-hidden' );
		}
	} else {
		items.forEach( ( item ) => {
			item.$element[ 0 ].removeAttribute( 'hidden' );
			item.$element.removeAttr( 'aria-hidden' );
		} );
	}
};