/**
 * Namespace for all classes, static methods and static properties.
 *
 * @namespace
 */
OO.ui = {};

OO.ui.bind = $.proxy;

/**
 * @property {Object}
 */
OO.ui.Keys = {
	UNDEFINED: 0,
	BACKSPACE: 8,
	DELETE: 46,
	LEFT: 37,
	RIGHT: 39,
	UP: 38,
	DOWN: 40,
	ENTER: 13,
	END: 35,
	HOME: 36,
	TAB: 9,
	PAGEUP: 33,
	PAGEDOWN: 34,
	ESCAPE: 27,
	SHIFT: 16,
	SPACE: 32
};

/**
 * Constants for MouseEvent.which
 *
 * @property {Object}
 */
OO.ui.MouseButtons = {
	LEFT: 1,
	MIDDLE: 2,
	RIGHT: 3
};

/**
 * @property {number}
 * @private
 */
OO.ui.elementId = 0;

/**
 * Generate a unique ID for element
 *
 * @return {string} ID
 */
OO.ui.generateElementId = function () {
	OO.ui.elementId++;
	return 'ooui-' + OO.ui.elementId;
};

/**
 * Check if an element is focusable.
 * Inspired by :focusable in jQueryUI v1.11.4 - 2015-04-14
 *
 * @param {jQuery} $element Element to test
 * @return {boolean} Element is focusable
 */
OO.ui.isFocusableElement = function ( $element ) {
	const element = $element[ 0 ];

	// Anything disabled is not focusable
	if ( element.disabled ) {
		return false;
	}

	// Check if the element is visible
	if ( !(
		// This is quicker than calling $element.is( ':visible' )
		$.expr.pseudos.visible( element ) &&
		// Check that all parents are visible
		!$element.parents().addBack().filter( function () {
			return $.css( this, 'visibility' ) === 'hidden';
		} ).length
	) ) {
		return false;
	}

	// Check if the element is ContentEditable, which is the string 'true'
	if ( element.contentEditable === 'true' ) {
		return true;
	}

	// Anything with a non-negative numeric tabIndex is focusable.
	// Use .prop to avoid browser bugs
	if ( $element.prop( 'tabIndex' ) >= 0 ) {
		return true;
	}

	// Some element types are naturally focusable
	// (indexOf is much faster than regex in Chrome and about the
	// same in FF: https://jsperf.com/regex-vs-indexof-array2)
	const nodeName = element.nodeName.toLowerCase();
	if ( [ 'input', 'select', 'textarea', 'button', 'object' ].indexOf( nodeName ) !== -1 ) {
		return true;
	}

	// Links and areas are focusable if they have an href
	if ( ( nodeName === 'a' || nodeName === 'area' ) && $element.attr( 'href' ) !== undefined ) {
		return true;
	}

	return false;
};

/**
 * Find a focusable child.
 *
 * @param {jQuery} $container Container to search in
 * @param {boolean} [backwards=false] Search backwards
 * @return {jQuery} Focusable child, or an empty jQuery object if none found
 */
OO.ui.findFocusable = function ( $container, backwards ) {
	let $focusable = $( [] ),
		// $focusableCandidates is a superset of things that
		// could get matched by isFocusableElement
		$focusableCandidates = $container
			.find( 'input, select, textarea, button, object, a, area, [contenteditable], [tabindex]' );

	if ( backwards ) {
		$focusableCandidates = Array.prototype.reverse.call( $focusableCandidates );
	}

	$focusableCandidates.each( ( i, el ) => {
		const $el = $( el );
		if ( OO.ui.isFocusableElement( $el ) ) {
			$focusable = $el;
			return false;
		}
	} );
	return $focusable;
};

/**
 * Get the user's language and any fallback languages.
 *
 * These language codes are used to localize user interface elements in the user's language.
 *
 * In environments that provide a localization system, this function should be overridden to
 * return the user's language(s). The default implementation returns English (en) only.
 *
 * @return {string[]} Language codes, in descending order of priority
 */
OO.ui.getUserLanguages = function () {
	return [ 'en' ];
};

/**
 * Get a value in an object keyed by language code.
 *
 * @param {Object.<string,any>} obj Object keyed by language code
 * @param {string|null} [lang] Language code, if omitted or null defaults to any user language
 * @param {string} [fallback] Fallback code, used if no matching language can be found
 * @return {any} Local value
 */
OO.ui.getLocalValue = function ( obj, lang, fallback ) {
	// Requested language
	if ( obj[ lang ] ) {
		return obj[ lang ];
	}
	// Known user language
	const langs = OO.ui.getUserLanguages();
	for ( let i = 0, len = langs.length; i < len; i++ ) {
		lang = langs[ i ];
		if ( obj[ lang ] ) {
			return obj[ lang ];
		}
	}
	// Fallback language
	if ( obj[ fallback ] ) {
		return obj[ fallback ];
	}
	// First existing language
	// eslint-disable-next-line no-unreachable-loop
	for ( lang in obj ) {
		return obj[ lang ];
	}

	return undefined;
};

/**
 * Check if a node is contained within another node.
 *
 * Similar to jQuery#contains except a list of containers can be supplied
 * and a boolean argument allows you to include the container in the match list
 *
 * @param {HTMLElement|HTMLElement[]} containers Container node(s) to search in
 * @param {HTMLElement} contained Node to find
 * @param {boolean} [matchContainers] Include the container(s) in the list of nodes to match,
 *  otherwise only match descendants
 * @return {boolean} The node is in the list of target nodes
 */
OO.ui.contains = function ( containers, contained, matchContainers ) {
	if ( !Array.isArray( containers ) ) {
		containers = [ containers ];
	}
	for ( let i = containers.length - 1; i >= 0; i-- ) {
		if (
			( matchContainers && contained === containers[ i ] ) ||
			$.contains( containers[ i ], contained )
		) {
			return true;
		}
	}
	return false;
};

/**
 * Return a function, that, as long as it continues to be invoked, will not
 * be triggered. The function will be called after it stops being called for
 * N milliseconds. If `immediate` is passed, trigger the function on the
 * leading edge, instead of the trailing.
 *
 * Ported from: http://underscorejs.org/underscore.js
 *
 * @param {Function} func Function to debounce
 * @param {number} [wait=0] Wait period in milliseconds
 * @param {boolean} [immediate] Trigger on leading edge
 * @return {Function} Debounced function
 */
OO.ui.debounce = function ( func, wait, immediate ) {
	let timeout;
	return function () {
		const context = this,
			args = arguments,
			later = function () {
				timeout = null;
				if ( !immediate ) {
					func.apply( context, args );
				}
			};
		if ( immediate && !timeout ) {
			func.apply( context, args );
		}
		if ( !timeout || wait ) {
			clearTimeout( timeout );
			timeout = setTimeout( later, wait );
		}
	};
};

/**
 * Puts a console warning with provided message.
 *
 * @param {string} message Message
 */
OO.ui.warnDeprecation = function ( message ) {
	if ( OO.getProp( window, 'console', 'warn' ) !== undefined ) {
		// eslint-disable-next-line no-console
		console.warn( message );
	}
};

/**
 * Returns a function, that, when invoked, will only be triggered at most once
 * during a given window of time. If called again during that window, it will
 * wait until the window ends and then trigger itself again.
 *
 * As it's not knowable to the caller whether the function will actually run
 * when the wrapper is called, return values from the function are entirely
 * discarded.
 *
 * @param {Function} func Function to throttle
 * @param {number} wait Throttle window length, in milliseconds
 * @return {Function} Throttled function
 */
OO.ui.throttle = function ( func, wait ) {
	let context, args, timeout,
		previous = Date.now() - wait;

	const run = function () {
		timeout = null;
		previous = Date.now();
		func.apply( context, args );
	};

	return function () {
		// Check how long it's been since the last time the function was
		// called, and whether it's more or less than the requested throttle
		// period. If it's less, run the function immediately. If it's more,
		// set a timeout for the remaining time -- but don't replace an
		// existing timeout, since that'd indefinitely prolong the wait.
		const remaining = Math.max( wait - ( Date.now() - previous ), 0 );
		context = this;
		args = arguments;
		if ( !timeout ) {
			// If time is up, do setTimeout( run, 0 ) so the function
			// always runs asynchronously, just like Promise#then .
			timeout = setTimeout( run, remaining );
		}
	};
};

/**
 * Reconstitute a JavaScript object corresponding to a widget created by
 * the PHP implementation.
 *
 * This is an alias for `OO.ui.Element.static.infuse()`.
 *
 * @param {string|HTMLElement|jQuery} node A single node for the widget to infuse.
 *   String must be a selector (deprecated).
 * @param {Object} [config] Configuration options
 * @return {OO.ui.Element}
 *   The `OO.ui.Element` corresponding to this (infusable) document node.
 */
OO.ui.infuse = function ( node, config ) {
	if ( typeof node === 'string' ) {
		// Deprecate passing a selector, which was accidentally introduced in Ibf95b0dee.
		// @since 0.41.0
		OO.ui.warnDeprecation(
			'Passing a selector to infuse is deprecated. Use an HTMLElement or jQuery collection instead.'
		);
	}
	return OO.ui.Element.static.infuse( node, config );
};

/**
 * Get a localized message.
 *
 * After the message key, message parameters may optionally be passed. In the default
 * implementation, any occurrences of $1 are replaced with the first parameter, $2 with the
 * second parameter, etc.
 * Alternative implementations of OO.ui.msg may use any substitution system they like, as long
 * as they support unnamed, ordered message parameters.
 *
 * In environments that provide a localization system, this function should be overridden to
 * return the message translated in the user's language. The default implementation always
 * returns English messages. An example of doing this with
 * [jQuery.i18n](https://github.com/wikimedia/jquery.i18n) follows.
 *
 *     @example
 *     const messagePath = 'oojs-ui/dist/i18n/',
 *         languages = [ $.i18n().locale, 'ur', 'en' ],
 *         languageMap = {};
 *
 *     for ( let i = 0, iLen = languages.length; i < iLen; i++ ) {
 *         languageMap[ languages[ i ] ] = messagePath + languages[ i ].toLowerCase() + '.json';
 *     }
 *
 *     $.i18n().load( languageMap ).done( function() {
 *         // Replace the built-in `msg` only once we've loaded the internationalization.
 *         // OOUI uses `OO.ui.deferMsg` for all initially-loaded messages. So long as
 *         // you put off creating any widgets until this promise is complete, no English
 *         // will be displayed.
 *         OO.ui.msg = $.i18n;
 *
 *         // A button displaying "OK" in the default locale
 *         const button = new OO.ui.ButtonWidget( {
 *             label: OO.ui.msg( 'ooui-dialog-message-accept' ),
 *             icon: 'check'
 *         } );
 *         $( document.body ).append( button.$element );
 *
 *         // A button displaying "OK" in Urdu
 *         $.i18n().locale = 'ur';
 *         button = new OO.ui.ButtonWidget( {
 *             label: OO.ui.msg( 'ooui-dialog-message-accept' ),
 *             icon: 'check'
 *         } );
 *         $( document.body ).append( button.$element );
 *     } );
 *
 * @param {string} key Message key
 * @param {...any} [params] Message parameters
 * @return {string} Translated message with parameters substituted
 */
OO.ui.msg = function ( key, ...params ) {
	// `OO.ui.msg.messages` is defined in code generated during the build process
	const messages = OO.ui.msg.messages;

	let message = messages[ key ];
	if ( typeof message === 'string' ) {
		// Perform $1 substitution
		message = message.replace( /\$(\d+)/g, ( unused, n ) => {
			const i = parseInt( n, 10 );
			return params[ i - 1 ] !== undefined ? params[ i - 1 ] : '$' + n;
		} );
	} else {
		// Return placeholder if message not found
		message = '[' + key + ']';
	}
	return message;
};

/**
 * Package a message and arguments for deferred resolution.
 *
 * Use this when you are statically specifying a message and the message may not yet be present.
 *
 * @param {string} key Message key
 * @param {...any} [params] Message parameters
 * @return {Function} Function that returns the resolved message when executed
 */
OO.ui.deferMsg = function () {
	// eslint-disable-next-line mediawiki/msg-doc
	return () => OO.ui.msg( ...arguments );
};

/**
 * Resolve a message.
 *
 * If the message is a function it will be executed, otherwise it will pass through directly.
 *
 * @param {Function|string|any} msg
 * @return {string|any} Resolved message when there was something to resolve, pass through
 *  otherwise
 */
OO.ui.resolveMsg = function ( msg ) {
	if ( typeof msg === 'function' ) {
		return msg();
	}
	return msg;
};

/**
 * @param {string} url
 * @return {boolean}
 */
OO.ui.isSafeUrl = function ( url ) {
	// Keep this function in sync with php/Tag.php

	function stringStartsWith( haystack, needle ) {
		return haystack.slice( 0, needle.length ) === needle;
	}

	const protocolAllowList = [
		'bitcoin', 'ftp', 'ftps', 'geo', 'git', 'gopher', 'http', 'https', 'irc', 'ircs',
		'magnet', 'mailto', 'mms', 'news', 'nntp', 'redis', 'sftp', 'sip', 'sips', 'sms', 'ssh',
		'svn', 'tel', 'telnet', 'urn', 'worldwind', 'xmpp'
	];

	if ( url === '' ) {
		return true;
	}

	for ( let i = 0; i < protocolAllowList.length; i++ ) {
		if ( stringStartsWith( url, protocolAllowList[ i ] + ':' ) ) {
			return true;
		}
	}

	// This matches '//' too
	if ( stringStartsWith( url, '/' ) || stringStartsWith( url, './' ) ) {
		return true;
	}
	if ( stringStartsWith( url, '?' ) || stringStartsWith( url, '#' ) ) {
		return true;
	}

	return false;
};

/**
 * Check if the user has a 'mobile' device.
 *
 * For our purposes this means the user is primarily using an
 * on-screen keyboard, touch input instead of a mouse and may
 * have a physically small display.
 *
 * It is left up to implementors to decide how to compute this
 * so the default implementation always returns false.
 *
 * @return {boolean} User is on a mobile device
 */
OO.ui.isMobile = function () {
	return false;
};

/**
 * Get the additional spacing that should be taken into account when displaying elements that are
 * clipped to the viewport, e.g. dropdown menus and popups. This is meant to be overridden to avoid
 * such menus overlapping any fixed headers/toolbars/navigation used by the site.
 *
 * @return {Object} Object with the properties 'top', 'right', 'bottom', 'left', each representing
 *  the extra spacing from that edge of viewport (in pixels)
 */
OO.ui.getViewportSpacing = function () {
	return {
		top: 0,
		right: 0,
		bottom: 0,
		left: 0
	};
};

/**
 * Get the element where elements that are positioned outside of normal flow are inserted,
 * for example dialogs and dropdown menus.
 *
 * This is meant to be overridden if the site needs to style this element in some way
 * (e.g. setting font size), and doesn't want to style the whole document.
 *
 * @return {HTMLElement}
 */
OO.ui.getTeleportTarget = function () {
	return document.body;
};

/**
 * Get the default overlay, which is used by various widgets when they are passed `$overlay: true`.
 * See <https://www.mediawiki.org/wiki/OOUI/Concepts#Overlays>.
 *
 * @return {jQuery} Default overlay node
 */
OO.ui.getDefaultOverlay = function () {
	if ( !OO.ui.$defaultOverlay ) {
		OO.ui.$defaultOverlay = $( '<div>' ).addClass( 'oo-ui-defaultOverlay' );
		$( OO.ui.getTeleportTarget() ).append( OO.ui.$defaultOverlay );
	}
	return OO.ui.$defaultOverlay;
};

// Define a custom HTML element that does nothing except to expose the `connectedCallback` callback
// as `onConnectOOUI` property. We use it in some widgets to detect when they are connected.
if ( window.customElements ) {
	window.customElements.define( 'ooui-connect-detector', class extends HTMLElement {
		connectedCallback() {
			if ( this.onConnectOOUI instanceof Function ) {
				this.onConnectOOUI();
			}
		}
	} );
}