/*!
 * Compact the interlanguage links in the sidebar
 *
 * Copyright (C) 2012-2014 Alolita Sharma, Amir Aharoni, Arun Ganesh, Brandon Harris,
 * Niklas Laxström, Pau Giner, Santhosh Thottingal, Siebrand Mazeland, Niharika Kohli
 * and other contributors. See CREDITS for a list.
 *
 * UniversalLanguageSelector is dual licensed GPLv2 or later and MIT. You don't
 * have to do anything special to choose one license or the other and you don't
 * have to notify anyone which license you are using. You are free to use
 * UniversalLanguageSelector in commercial projects as long as the copyright
 * header is left intact. See files GPL-LICENSE and MIT-LICENSE for details.
 *
 * @file
 * @ingroup Extensions
 * @licence GNU GPL-2.0-or-later
 * @licence MIT License
 */

( function () {
	'use strict';

	var DEFAULT_LIST_SIZE = 9;

	/**
	 * @param {Array} target
	 * @param {Array} source
	 * @param {string|string[]|undefined} items Language code, or list of language codes
	 */
	function addMatchWithoutDuplicate( target, source, items ) {
		var i;
		if ( items === undefined ) {
			return;
		}
		items = !Array.isArray( items ) ? [ items ] : items;
		for ( i = 0; i < items.length; i++ ) {
			if (
				// Only add if unique and matches source
				target.indexOf( items[ i ] ) === -1 &&
				source.indexOf( items[ i ] ) !== -1
			) {
				target.push( items[ i ] );
			}
		}
	}

	/**
	 * Normalize a language code for ULS usage.
	 *
	 * MediaWiki language codes (especially on WMF sites) are inconsistent
	 * with ULS codes. We need to use ULS codes to access the proper data.
	 *
	 * @param {string} code
	 * @return {string} Normalized language code
	 */
	function convertMediaWikiLanguageCodeToULS( code ) {
		code = code.toLowerCase();
		return $.uls.data.isRedirect( code ) || code;
	}

	/**
	 * Get user-defined assistant languages on wikis with Translate extension.
	 *
	 * Where available, they're languages deemed useful by the user.
	 *
	 * @return {string[]|undefined} Language codes
	 */
	function getAssistantLanguages() {
		var assistantLanguages = mw.user.options.get( 'translate-editlangs' );
		if ( !assistantLanguages || assistantLanguages === 'default' ) {
			return;
		}

		return assistantLanguages.split( /,\s*/ );
	}

	/**
	 * Get previously selected languages.
	 *
	 * Previous languages are a good suggestion because the user has
	 * explicitly chosen them in the past.
	 *
	 * @return {string[]} Language codes
	 */
	function getPreviousLanguages() {
		return mw.uls.getPreviousLanguages();
	}

	/**
	 * Get languages from the Babel box on the user's user page.
	 *
	 * @return {string[]|undefined} Language codes
	 */
	function getBabelLanguages() {
		return mw.config.get( 'wgULSBabelLanguages' );
	}

	/**
	 * Get site-specific highlighted languags. Mostly used on Wikimedia sites.
	 *
	 * @return {string[]|undefined} Language codes
	 */
	function getSitePicks() {
		return mw.config.get( 'wgULSCompactLinksPrepend' );
	}

	/**
	 * Get probable languages predicted by ULS.
	 *
	 * @return {string[]} Language codes
	 */
	function getCommonLanguages() {
		return mw.uls.getFrequentLanguageList();
	}

	/**
	 * Get globally common languages.
	 *
	 * These are not user-specific. This helps to avoid biasing the compact list
	 * to language codes that sort to the beginning of the alphabet in the
	 * final stage.
	 *
	 * @return {string[]} Language codes
	 */
	function getExtraCommonLanguages() {
		return [
			'zh', 'en', 'hi', 'ur', 'es', 'ar', 'ru', 'id', 'ms', 'pt',
			'fr', 'de', 'bn', 'ja', 'pnb', 'pa', 'jv', 'te', 'ta', 'ko', 'mr', 'tr', 'vi',
			'it', 'fa', 'sv', 'nl', 'pl'
		];
	}

	/**
	 * The final strategy is the original interlanguage list.
	 *
	 * @param {string[]} languages Language codes
	 * @return {string[]} Language codes
	 */
	function getFinalFallback( languages ) {
		return languages;
	}

	/**
	 * @class
	 * @constructor
	 * @param {HTMLElement} listElement Interlanguage list element
	 * @param {Object} options
	 */
	function CompactInterlanguageList( listElement, options ) {
		this.listElement = listElement;
		this.options = options || {};

		/**
		 * @private
		 * @property {Object} interlanguageList
		 */
		this.interlanguageList = null;

		/**
		 * @private
		 * @property {Object} interlanguageList
		 */
		this.compactList = null;

		this.commonInterlanguageList = null;
		this.$trigger = null;
		this.compactSize = 0;
		this.listSize = 0;
	}

	/**
	 * Initialize the plugin
	 */
	CompactInterlanguageList.prototype.init = function () {
		var max = this.options.max || DEFAULT_LIST_SIZE;

		this.interlanguageList = this.getInterlanguageList();
		this.listSize = Object.keys( this.interlanguageList ).length;

		if ( this.listSize <= max ) {
			// Not enough languages to compact the list
			mw.hook( 'mw.uls.compactlinks.initialized' ).fire( false );
			return;
		}

		// If we're only a bit beyond max, limit to 7 instead of 9.
		// FIXME: This assumes the max is 9.
		this.compactSize = ( this.listSize <= 12 ) ? 7 : max;
		this.compactList = this.getCompactList();
		this.hideOriginal();
		this.render();
		this.listen();
	};

	/**
	 * Render the compacted interlanguage list and triggers
	 */
	CompactInterlanguageList.prototype.render = function () {
		var language;

		for ( language in this.compactList ) {
			this.compactList[ language ].parentNode.style.display = '';
		}

		this.addTrigger();

		mw.hook( 'mw.uls.compactlinks.initialized' ).fire( true );
	};

	/**
	 * Attaches the actual selector to the trigger.
	 *
	 * @param {jQuery} $trigger Element to use as trigger.
	 */
	CompactInterlanguageList.prototype.createSelector = function ( $trigger ) {
		var languages = Object.keys( this.interlanguageList ),
			self = this,
			ulsLanguageList = {};

		$.each( this.interlanguageList, function ( languageCode, el ) {
			ulsLanguageList[ languageCode ] = el.textContent;
		} );

		// Attach ULS to the trigger
		$trigger.uls( {
			onReady: function () {
				this.$menu.addClass( 'interlanguage-uls-menu' );
			},
			/**
			 * Language selection handler
			 *
			 * @param {string} language language code
			 * @param {Object} event jQuery event object
			 */
			onSelect: function ( language, event ) {
				self.$trigger.removeClass( 'selector-open' );
				mw.uls.addPreviousLanguage( language );

				// Switch the current tab to the new language,
				// unless it was Ctrl-click or Command-click
				if ( !event.metaKey && !event.shiftKey ) {
					location.href = self.interlanguageList[ language ].href;
				}
			},
			onVisible: function () {
				var offset, height, width, triangleWidth;
				// The panel is positioned carefully so that our pointy triangle,
				// which is implemented as a square box rotated 45 degrees with
				// rotation origin in the middle. See the corresponding style file.

				// These are for the trigger
				offset = $trigger.offset();
				width = $trigger.outerWidth();
				height = $trigger.outerHeight();

				// Triangle width is: who knows now, but this still looks fine.
				triangleWidth = 12;

				if ( offset.left > $( window ).width() / 2 ) {
					this.left = offset.left - this.$menu.outerWidth() - triangleWidth;
					this.$menu.removeClass( 'selector-left' ).addClass( 'selector-right' );
				} else {
					this.left = offset.left + width + triangleWidth;
					this.$menu.removeClass( 'selector-right' ).addClass( 'selector-left' );
				}
				// Offset from the middle of the trigger
				this.top = offset.top + ( height / 2 ) - 27;

				this.$menu.css( {
					left: this.left,
					top: this.top
				} );
				$trigger.addClass( 'selector-open' );
			},
			languageDecorator: function ( $languageLink, language ) {
				var element = self.interlanguageList[ language ];
				// Set href, text, and tooltip exactly same as what was in
				// interlanguage link. The ULS autonym might be different in some
				// cases like sr. In ULS it is "српски", while in interlanguage links
				// it is "српски / srpski"
				$languageLink
					.prop( {
						href: element.href,
						title: element.title
					} )
					.text( element.textContent );

				// This code is to support badges used in Wikimedia
				$languageLink.parent().addClass( element.parentNode.className );
			},
			onCancel: function () {
				$trigger.removeClass( 'selector-open' );
			},
			languages: ulsLanguageList,
			ulsPurpose: 'compact-language-links',
			// Show common languages
			quickList: self.getCommonLanguages( languages ),
			noResultsTemplate: function () {
				var $defaultTemplate = $.fn.lcd.defaults.noResultsTemplate.call( this );
				// Customize the message
				$defaultTemplate
					.find( '.uls-no-results-found-title' )
					.data( 'i18n', 'ext-uls-compact-no-results' );
				return $defaultTemplate;
			}
		} );
	};

	/**
	 * Bind to event handlers and listen for events
	 */
	CompactInterlanguageList.prototype.listen = function () {
		var self = this;

		this.$trigger.one( 'click', function () {
			// Load the ULS now.
			mw.loader.using( 'ext.uls.mediawiki' ).then( function () {
				self.createSelector( self.$trigger );
				self.$trigger.click();
			} );
		} );
	};

	/**
	 * Get the compacted interlanguage list as associative array
	 *
	 * @return {Object}
	 */
	CompactInterlanguageList.prototype.getCompactList = function () {
		var language, languages, compactLanguages, i, compactedList;

		compactedList = {};
		languages = Object.keys( this.interlanguageList );
		compactLanguages = this.compact( languages );

		for ( i = 0; i < compactLanguages.length; i++ ) {
			language = compactLanguages[ i ];
			compactedList[ language ] = this.interlanguageList[ language ];
		}

		return compactedList;
	};

	/**
	 * Get compacting strategies.
	 *
	 * The items will be executed in the given order till the required
	 * compact size is achieved. Each strategy is given two arrays: `candidates`
	 * and `languages`. The candidates array is a list the callback should add to.
	 * The languages list contains language codes actually available for the current
	 * page, the callback may use this to optimise their search for candidates,
	 * although compact() will filter out irrelevant candidates so strategies should
	 * only use this if it helps narrow their search for candidates, avoid needless
	 * filtering that compact() will do already.
	 *
	 * @return {Function[]} Array of compacting functions
	 */
	CompactInterlanguageList.prototype.getCompactStrategies = function () {
		return [
			getAssistantLanguages,
			getPreviousLanguages,
			getBabelLanguages,
			getSitePicks,
			getCommonLanguages,
			this.getLangsInText,
			this.getLangsWithBadges,
			getExtraCommonLanguages,
			getFinalFallback
		];
	};

	/**
	 * Compact a given array of languages
	 *
	 * @param {Array} languages
	 * @return {Array} Compacted array
	 */
	CompactInterlanguageList.prototype.compact = function ( languages ) {
		var i, strategies, found,
			compactLanguages = [];

		strategies = this.getCompactStrategies();
		for ( i = 0; i < strategies.length; i++ ) {
			found = strategies[ i ]( languages );
			// Add language codes from 'found' that are also in 'languages'
			// to 'compactLanguages' (if not already in there).
			addMatchWithoutDuplicate( compactLanguages, languages, found );
			if ( compactLanguages.length >= this.compactSize ) {
				// We have more than enough items. Stop here.
				compactLanguages = compactLanguages.slice( 0, this.compactSize );
				break;
			}
		}

		return compactLanguages;
	};

	/**
	 * Get language codes that are used in the page's text content.
	 *
	 * This is done by looking for HTML elements with a "lang" attribute—they
	 * are likely to appear in a foreign name, for example.
	 *
	 * The reader doesn't necessarily know this language, but it
	 * appears relevant to the page.
	 *
	 * @return {string[]} Language codes
	 */
	CompactInterlanguageList.prototype.getLangsInText = function () {
		var languagesInText = [];
		$.each( document.querySelectorAll( '#mw-content-text [lang]' ), function ( i, el ) {
			var lang = convertMediaWikiLanguageCodeToULS( el.lang );
			if ( languagesInText.indexOf( lang ) === -1 ) {
				languagesInText.push( lang );
			}
		} );

		return languagesInText;
	};

	/**
	 * Get languages in which a related page has any kind of a badge,
	 * such as "featured article". The "badge-*" classes are added by Wikibase.
	 *
	 * @return {string[]} Language codes
	 */
	CompactInterlanguageList.prototype.getLangsWithBadges = function () {
		return $.map(
			document.querySelectorAll( '#p-lang [class*="badge"]' ),
			function ( el ) {
				return convertMediaWikiLanguageCodeToULS(
					el.querySelector( '.interlanguage-link-target' ).lang
				);
			}
		);
	};

	/**
	 * Get the list of languages links.
	 *
	 * @return {Object} Map of language codes to elements.
	 */
	CompactInterlanguageList.prototype.getInterlanguageList = function () {
		var interlanguageList = {};

		$.each( this.listElement.querySelectorAll( '.interlanguage-link-target' ), function ( i, el ) {
			var langCode = convertMediaWikiLanguageCodeToULS( el.lang );
			interlanguageList[ langCode ] = el;
		} );

		return interlanguageList;
	};

	/**
	 * Get common languages - the most probable languages predicted by ULS.
	 *
	 * @param {string[]} languages Language codes
	 * @return {string[]} List of all common language codes
	 */
	CompactInterlanguageList.prototype.getCommonLanguages = function ( languages ) {
		if ( this.commonInterlanguageList === null ) {
			this.commonInterlanguageList = mw.uls.getFrequentLanguageList()
				.filter( function ( language ) {
					return languages.indexOf( language ) >= 0;
				} );
		}

		return this.commonInterlanguageList;
	};

	/**
	 * Hide languages in the interlanguage list.
	 *
	 * The most relevant ones are unhidden in #render.
	 */
	CompactInterlanguageList.prototype.hideOriginal = function () {
		var links = this.listElement.querySelectorAll( '.interlanguage-link' ),
			i = links.length;
		while ( i-- ) {
			links[ i ].style.display = 'none';
		}
	};

	/**
	 * Add the trigger at the bottom of the language list
	 */
	CompactInterlanguageList.prototype.addTrigger = function () {
		var trigger = document.createElement( 'button' );
		trigger.className = 'mw-interlanguage-selector mw-ui-button';
		trigger.title = mw.message( 'ext-uls-compact-link-info' ).plain();
		// Use text() because the message needs {{PLURAL:}}
		trigger.textContent = mw.message(
			'ext-uls-compact-link-count',
			mw.language.convertNumber( this.listSize - this.compactSize )
		).text();

		this.listElement.appendChild( trigger );
		this.$trigger = $( trigger );
	};

	/**
	 * Performance cost of calling createCompactList(), as of 2018-09-10.
	 *
	 * Summary:
	 * - DOM Queries: 5 + 1N
	 *   * createCompactList (1 querySelector)
	 *   * getLangsWithBadges (1N querySelector, 1 querySelectorAll)
	 *   * getInterlanguageList (1 querySelectorAll)
	 *   * getLangsInText (1 querySelectorAll)
	 *   * hideOriginal (1 querySelectorAll)
	 * - DOM Writes: 1 + 2N
	 *   * addTrigger (1 appendChild)
	 *   * hideOriginal (1N Element.style)
	 *   * render (1N Element.style)
	 * - Misc: 1
	 *   * addTrigger (1 mw.Message#parser)
	 */
	function createCompactList() {
		var listElement, compactList;
		listElement = document.querySelector( '#p-lang ul' );
		if ( !listElement ) {
			// Not all namespaces/pages/actions have #p-lang.
			return;
		}
		compactList = new CompactInterlanguageList( listElement, {
			// Compact the list to this size
			max: 9
		} );
		compactList.init();

	}

	// Early execute of createCompactList
	if ( document.readyState === 'interactive' ) {
		createCompactList();
	} else {
		$( createCompactList );
	}

}() );
