const features = require( './features.js' );
const PINNED_HEADER_CLASS = 'vector-pinnable-header-pinned';
const UNPINNED_HEADER_CLASS = 'vector-pinnable-header-unpinned';
const popupNotification = require( './popupNotification.js' );

/**
 * Callback for matchMedia listener that overrides the pinnable header's stored state
 * at a certain breakpoint and forces it to unpin.
 * Usage of 'e.matches' assumes a `max-width` not `min-width` media query.
 *
 * @param {HTMLElement} header
 * @param {MediaQueryList|MediaQueryListEvent} e
 */
function disablePinningAtBreakpoint( header, e ) {
	const {
		pinnableElementId,
		pinnedContainerId,
		unpinnedContainerId,
		featureName
	} = header.dataset;
	const savedPinnedState = JSON.parse( header.dataset.savedPinnedState || 'false' );

	// (typescript null check)
	if ( !( pinnableElementId && unpinnedContainerId && pinnedContainerId && featureName ) ) {
		return;
	}

	if ( e.matches && savedPinnedState === true ) {
		features.toggleDocClasses( featureName, false );
		movePinnableElement( pinnableElementId, unpinnedContainerId );
	}

	if ( !e.matches && savedPinnedState === true ) {
		features.toggleDocClasses( featureName, true );
		movePinnableElement( pinnableElementId, pinnedContainerId );
	}
}

/**
 * Saves the persistent pinnable state in the element's dataset
 * so that it can be overridden at lower resolutions and the
 * reverted to at wider resolutions.
 *
 * This is not necessarily the elements current state, but it
 * seeks to represent the state of the saved user preference.
 *
 * @param {HTMLElement} header
 */
function setSavedPinnableState( header ) {
	header.dataset.savedPinnedState = String( isPinned( header ) );
}

/**
 * Toggle classes on the body and pinnable element
 *
 * @param {HTMLElement} header pinnable element
 */
function togglePinnableClasses( header ) {
	const featureName = /** @type {string} */ ( header.dataset.featureName );

	// Leverage features.js to toggle the body classes and persist the state
	// for logged-in users.
	features.toggle( featureName );

	// Toggle pinned class
	header.classList.toggle( PINNED_HEADER_CLASS );
	header.classList.toggle( UNPINNED_HEADER_CLASS );
}

/**
 * Create the indicators for the pinnable element
 *
 * @param {string} pinnableElementId
 */
function addPinnableElementIndicator( pinnableElementId ) {
	const dropdownSelector = document.querySelector( `#${ pinnableElementId }-dropdown` );
	const container = dropdownSelector && dropdownSelector.parentElement;
	if ( container ) {
		// Possible messages include:
		// * vector-page-tools-unpinned-popup
		// * vector-main-menu-unpinned-popup
		const message = mw.msg( `${ pinnableElementId }-unpinned-popup` );
		popupNotification.add( container, message, pinnableElementId )
			.then( ( popupWidget ) => {
				if ( popupWidget ) {
					popupNotification.show( popupWidget );
				}
			} );
	}
}

/**
 * Event handler that toggles the pinnable elements pinned state.
 * Also moves the pinned element when those params are provided
 * (via data attributes).
 *
 * @param {HTMLElement} header PinnableHeader element.
 */
function pinnableElementClickHandler( header ) {
	const {
		pinnableElementId,
		pinnedContainerId,
		unpinnedContainerId
	} = header.dataset;

	togglePinnableClasses( header );

	const isPinnedElement = isPinned( header );
	// Optional functionality of moving the pinnable element in the DOM
	// to different containers based on it's pinned status
	if ( pinnableElementId && pinnedContainerId && unpinnedContainerId ) {
		setSavedPinnableState( header );
		const newContainerId = isPinnedElement ? pinnedContainerId : unpinnedContainerId;
		movePinnableElement( pinnableElementId, newContainerId );
		window.dispatchEvent( new Event( 'resize' ) );
		setFocusAfterToggle( pinnableElementId );
		if ( !isPinnedElement ) {
			addPinnableElementIndicator( pinnableElementId );
		}
	}
}

/**
 * Sets focus on the correct toggle button depending on the pinned state.
 * Also opens the dropdown containing the unpinned element.
 *
 * @param {string} pinnableElementId
 */
function setFocusAfterToggle( pinnableElementId ) {
	let focusElement;
	const pinnableElement = document.getElementById( pinnableElementId );
	const header = /** @type {HTMLElement|null} */ ( pinnableElement && pinnableElement.querySelector( '.vector-pinnable-header' ) );
	if ( !pinnableElement || !header ) {
		return;
	}
	if ( isPinned( header ) ) {
		focusElement = /** @type {HTMLElement|null} */ ( pinnableElement.querySelector( '.vector-pinnable-header-unpin-button' ) );
	} else {
		const dropdown = pinnableElement.closest( '.vector-dropdown' );
		focusElement = /** @type {HTMLInputElement|null} */ ( dropdown && dropdown.querySelector( '.vector-menu-checkbox' ) );
	}
	if ( focusElement ) {
		focusElement.focus();
	}
}

/**
 * Binds all the toggle buttons in a pinnableElement
 * to the click handler that enables pinnability.
 *
 * @param {HTMLElement} header
 */
function bindPinnableToggleButtons( header ) {
	const toggleButtons = header.querySelectorAll( '.vector-pinnable-header-toggle-button' );
	toggleButtons.forEach( ( button ) => {
		button.addEventListener( 'click', pinnableElementClickHandler.bind( null, header ) );
	} );
}

/**
 * Binds pinnable breakpoint to allow automatic unpinning
 * of pinnable elements with pinnedContainerId and unpinnedContainerId defined
 *
 * @param {HTMLElement} header
 */
function bindPinnableBreakpoint( header ) {
	const { pinnedContainerId, unpinnedContainerId } = header.dataset;
	if ( !unpinnedContainerId || !pinnedContainerId ) {
		return;
	}

	const pinnableBreakpoint = window.matchMedia( '(max-width: 1119px)' );
	// Set saved pinned state for narrow breakpoint behaviour.
	setSavedPinnableState( header );
	// Check the breakpoint in case an override is needed on pageload.
	disablePinningAtBreakpoint( header, pinnableBreakpoint );

	// Add match media handler.
	if ( pinnableBreakpoint.addEventListener ) {
		pinnableBreakpoint.addEventListener( 'change', disablePinningAtBreakpoint.bind( null, header ) );
	} else {
		// Before Safari 14, MediaQueryList is based on EventTarget,
		// so you must use addListener() and removeListener() to observe media query lists.
		pinnableBreakpoint.addListener( disablePinningAtBreakpoint.bind( null, header ) );
	}
}

/**
 * @param {HTMLElement} header
 * @return {boolean} Returns true if the element is pinned and false otherwise.
 */
function isPinned( header ) {
	const featureName = /** @type {string} */ ( header.dataset.featureName );
	return features.isEnabled( featureName );
}

/**
 * Ensures the header classes are in sync with the pinnable headers state
 * in the case that it's moved via movePinnableElement().
 *
 * @param {HTMLElement} pinnableElement
 */
function updatePinnableHeaderClass( pinnableElement ) {
	const header = pinnableElement.querySelector( '.vector-pinnable-header' );

	// Because Typescript
	if ( !header || !( header instanceof HTMLElement ) ) {
		return;
	}

	// Toggle header classes
	if ( isPinned( header ) ) {
		header.classList.add( PINNED_HEADER_CLASS );
		header.classList.remove( UNPINNED_HEADER_CLASS );
	} else {
		header.classList.remove( PINNED_HEADER_CLASS );
		header.classList.add( UNPINNED_HEADER_CLASS );
	}
}

/**
 * @param {string} pinnableElementId
 * @param {string} newContainerId
 */
function movePinnableElement( pinnableElementId, newContainerId ) {
	const pinnableElem = document.getElementById( pinnableElementId );
	const newContainer = document.getElementById( newContainerId );
	const currContainer = /** @type {HTMLElement} */ ( pinnableElem && pinnableElem.parentElement );

	if ( !pinnableElem || !newContainer || !currContainer ) {
		return;
	}

	// Avoid moving element if unnecessary
	if ( currContainer.id !== newContainerId ) {
		newContainer.insertAdjacentElement( 'beforeend', pinnableElem );
		updatePinnableHeaderClass( pinnableElem );
	}

	popupNotification.hideAll();
}

/**
 * Update the pinnable element location in the DOM based off of whether its pinned or not.
 * This is only necessary with pinnable elements that use client preferences (i.e. appearance menu)
 * as all other pinnable elements should be serverside rendered in the correct location
 *
 * @param {HTMLElement} header
 */
function updatePinnableElementLocation( header ) {
	const newContainerId = isPinned( header ) ?
		header.dataset.pinnedContainerId :
		header.dataset.unpinnedContainerId;
	if ( header.dataset.pinnableElementId && newContainerId ) {
		movePinnableElement( header.dataset.pinnableElementId, newContainerId );
	}
}

function initPinnableElement() {
	const pinnableHeader = /** @type {NodeListOf<HTMLElement>} */ ( document.querySelectorAll( '.vector-pinnable-header' ) );
	pinnableHeader.forEach( ( header ) => {
		if ( header.dataset.featureName && header.dataset.pinnableElementId ) {
			bindPinnableToggleButtons( header );
			bindPinnableBreakpoint( header );
			updatePinnableElementLocation( header );
		}
	} );
}

// T349924: Remove hasPinnedElements after one cycle of analyticsPinnedState() merge.
/**
 * Checks if at least one of the elements in the HTML document is pinned based on CSS class names.
 *
 * @method
 * @return {boolean} True if at least one pinned element is found, otherwise false.
 */
function hasPinnedElements() {
	const suffixesToCheck = [ 'pinned-clientpref-1', 'pinned-enabled' ];
	const htmlElement = document.documentElement;
	return Array.from( htmlElement.classList ).some(
		( className ) => suffixesToCheck.some( ( suffix ) => className.endsWith( suffix ) )
	);
}

/**
 * @stable for use in WikimediaEvents only.
 * Checks if at least one of the elements in the HTML document is pinned based on CSS class names.
 *
 * @method
 * @return {boolean} True if at least one pinned element is found, otherwise false.
 */
function analyticsPinnedState() {
	const htmlElement = document.documentElement;
	return htmlElement.classList.contains( 'vector-feature-main-menu-pinned-enabled' ) || htmlElement.classList.contains( 'vector-feature-page-tools-pinned-enabled' );
}

module.exports = {
	// T349924: Remove hasPinnedElements.
	hasPinnedElements,
	analyticsPinnedState,
	initPinnableElement,
	movePinnableElement,
	setFocusAfterToggle,
	isPinned,
	PINNED_HEADER_CLASS,
	UNPINNED_HEADER_CLASS
};
