( function () {
	'use strict';

	var groupsLoader, delay;

	/**
	 * options
	 *  - position: accepts same values as jquery.ui.position
	 *  - onSelect:
	 *  - language:
	 *  - preventSelector: boolean to not allow selection of subgroups.
	 *  - recent: list of recent group ids
	 * groups: list of message group ids
	 *
	 * @param {Element} element
	 * @param {Object} options
	 * @param {Object} [options.position] Accepts same values as jquery.ui.position.
	 * @param {Function} [options.onSelect] Callback with message group id when selected.
	 * @param {string} options.language Language code for statistics.
	 * @param {boolean} [options.preventSelector] Do not allow selection of subgroups.
	 * @param {string[]} [options.recent] List of recent message group ids.
	 * @param {string[]} [groups] List of message group ids to show.
	 */
	function TranslateMessageGroupSelector( element, options, groups ) {
		this.$trigger = $( element );
		this.$menu = null;
		this.$search = null;
		this.$list = null;
		this.$loader = null;

		this.parentGroupId = null;
		this.options = $.extend( true, {}, $.fn.msggroupselector.defaults, options );
		// Store the explicitly given options, which can be passed to subgroup
		// selectors.
		this.customOptions = options;
		this.flatGroupList = null;
		this.groups = groups;
		this.firstShow = true;

		this.init();
	}

	TranslateMessageGroupSelector.prototype = {
		constructor: TranslateMessageGroupSelector,

		/**
		 * Initialize the plugin
		 */
		init: function () {
			this.parentGroupId = this.$trigger.data( 'msggroupid' );
			this.prepareSelectorMenu();
			this.listen();
		},

		/**
		 * Prepare the selector menu rendering
		 */
		prepareSelectorMenu: function () {
			this.$menu = $( '<div>' )
				.addClass( 'tux-groupselector' )
				.addClass( 'grid hide' );

			var $searchIcon = $( '<div>' )
				.addClass( 'two columns tux-groupselector__filter__search__icon' );

			this.$search = $( '<input>' )
				.prop( 'type', 'text' )
				.addClass( 'tux-groupselector__filter__search__input' )
				.prop( 'placeholder', mw.msg( 'translate-msggroupselector-search-placeholder' ) );

			var $search = $( '<div>' )
				.addClass( 'ten columns' )
				.append( this.$search );

			var $listFilters = $( '<div>' )
				.addClass( 'tux-groupselector__filter__tabs' )
				.addClass( 'six columns' )
				.append(
					$( '<div>' )
						.addClass( 'tux-grouptab tux-grouptab--all tux-grouptab--selected' )
						.text( mw.msg( 'translate-msggroupselector-search-all' ) )
				);

			if ( this.options.recent && this.options.recent.length ) {
				$listFilters.append(
					$( '<div>' )
						.addClass( 'tux-grouptab tux-grouptab--recent' )
						.text( mw.msg( 'translate-msggroupselector-search-recent' ) )
				);
			}

			var $searchGroup = $( '<div>' )
				.addClass( 'tux-groupselector__filter__search' )
				.addClass( 'six columns' )
				.append( $searchIcon, $search );

			var $listFiltersGroup = $( '<div>' )
				.addClass( 'tux-groupselector__filter' )
				.addClass( 'row' )
				.append( $listFilters, $searchGroup );

			this.$list = $( '<div>' )
				.addClass( 'tux-grouplist' )
				.addClass( 'row' );

			this.$loader = $( '<div>' )
				.addClass( 'tux-loading-indicator tux-loading-indicator--centered' );

			this.$menu.append( $listFiltersGroup, this.$loader, this.$list );

			this.$menu.appendTo( document.body );
		},

		/**
		 * Show the selector
		 */
		show: function () {
			this.$menu.addClass( 'open' ).removeClass( 'hide' );
			this.position();
			// Place the focus in the message group search box.
			this.$search.trigger( 'focus' );
			// Start loading the groups, but assess the situation again after
			// they are loaded, in case user has made further interactions.
			if ( this.firstShow ) {
				this.loadGroups().done( this.showList.bind( this ) );
				this.firstShow = false;
			}
		},

		/**
		 * Hide the selector
		 *
		 * @param {jQuery.Event} e
		 */
		hide: function ( e ) {
			// Do not hide if the trigger is clicked
			if ( e && ( this.$trigger.is( e.target ) || this.$trigger.has( e.target ).length ) ) {
				return;
			}

			this.$menu.addClass( 'hide' ).removeClass( 'open' );
		},

		/**
		 * Toggle the menu open/close state
		 */
		toggle: function () {
			if ( this.$menu.hasClass( 'open' ) ) {
				this.hide();
			} else {
				this.show();
			}
		},

		/**
		 * Attach event listeners
		 */
		listen: function () {
			var groupSelector = this;

			// Hide the selector panel when clicking outside of it
			$( document.documentElement ).on( 'click', this.hide.bind( this ) );

			groupSelector.$trigger.on( 'click', function () {
				groupSelector.toggle();
			} );

			groupSelector.$menu.on( 'click', function ( e ) {
				e.preventDefault();
				e.stopPropagation();
			} );

			// Handle click on row item. This selects the group, and in case it has
			// subgroups, also opens a new menu to show them.
			groupSelector.$menu.on( 'click', '.tux-grouplist__item', function () {
				var messageGroup = $( this ).data( 'msggroup' );

				groupSelector.hide();

				groupSelector.$trigger.nextAll().remove();

				if ( !groupSelector.options.preventSelector ) {
					var $newLink = $( '<span>' )
						.addClass( 'grouptitle grouplink' )
						.text( messageGroup.label )
						.data( 'msggroupid', messageGroup.id );

					groupSelector.$trigger.after( $newLink );

					if ( messageGroup.groups && messageGroup.groups.length > 0 ) {
						// Show the new menu immediately.
						// Pass options for callbacks, language etc. but ignore the position
						// option unless explicitly given to allow automatic recalculation
						// of the position compared to the new trigger.
						$newLink
							.addClass( 'tux-breadcrumb__item--aggregate' )
							.msggroupselector( groupSelector.customOptions )
							.data( 'msggroupselector' ).show();
					}
				}

				if ( groupSelector.options.onSelect ) {
					groupSelector.options.onSelect( messageGroup );
				}
			} );

			// Handle the tabs All | Recent
			var $tabs = groupSelector.$menu.find( '.tux-grouptab' );
			$tabs.on( 'click', function () {
				var $this = $( this );

				/* Do nothing if user clicks the active tab.
				 * Fixes two things:
				 * - The blue bottom border highlight doesn't jump around
				 * - No flash when clicking recent tab again
				 */
				if ( $this.hasClass( 'tux-grouptab--selected' ) ) {
					return;
				}

				// This is okay as long as we only have two classes
				$tabs.toggleClass( 'tux-grouptab--selected' );
				groupSelector.$search.val( '' );
				groupSelector.showList();
			} );

			this.$search.on( 'click', this.show.bind( this ) )
				.on( 'keypress', this.keyup.bind( this ) )
				.on( 'keyup', this.keyup.bind( this ) );

			if ( this.eventSupported( 'keydown' ) ) {
				this.$search.on( 'keydown', this.keyup.bind( this ) );
			}
		},

		/**
		 * Handle the keypress/keyup events in the message group search box.
		 */
		keyup: function () {
			delay( this.showList.bind( this ), 300 );
		},

		/**
		 * Position the menu
		 */
		position: function () {
			if ( this.options.position.of === undefined ) {
				// eslint-disable-next-line no-jquery/variable-pattern
				this.options.position.of = this.$trigger;
			}
			this.$menu.position( this.options.position );
		},

		/**
		 * Shows suitable list for current view, taking possible filter into account
		 */
		showList: function () {
			var query = this.$search.val().trim().toLowerCase();

			if ( query ) {
				this.filter( query );
			} else {
				this.showUnfilteredList();
			}
		},

		/**
		 * Shows an unfiltered list of groups depending on the selected tab.
		 */
		showUnfilteredList: function () {
			var $selected = this.$menu.find( '.tux-grouptab--selected' );

			if ( $selected.hasClass( 'tux-grouptab--all' ) ) {
				if ( this.groups ) {
					this.showSelectedGroups( this.groups );
				} else {
					this.showDefaultGroups();
				}
			} else if ( $selected.hasClass( 'tux-grouptab--recent' ) ) {
				this.showRecentGroups();
			}
		},

		/**
		 * Shows the list of message groups excluding subgroups.
		 *
		 * In case a parent message group has been given, only subgroups of that
		 * message group are shown, otherwise all top-level message groups are shown.
		 */
		showDefaultGroups: function () {
			var groupSelector = this;

			this.$loader.removeClass( 'hide' );

			this.loadGroups().done( function ( groups ) {
				var groupsToShow = mw.translate.findGroup( groupSelector.parentGroupId, groups );

				// We do not want to display the group itself, only its subgroups
				if ( groupSelector.parentGroupId ) {
					groupsToShow = groupsToShow.groups;
				}

				groupSelector.$loader.addClass( 'hide' );
				groupSelector.$list.empty();
				groupSelector.addGroupRows( groupsToShow );
			} );
		},

		/**
		 * Show recent message groups.
		 */
		showRecentGroups: function () {
			var recent = this.options.recent || [];

			this.showSelectedGroups( recent );
		},

		/**
		 * Load message groups.
		 *
		 * @param {Array} groups List of the message group ids to show.
		 */
		showSelectedGroups: function ( groups ) {
			var groupSelector = this;
			this.$loader.removeClass( 'hide' );
			this.loadGroups()
				.then( function ( allGroups ) {
					var rows = [];
					groups.forEach( function ( id ) {
						var group = mw.translate.findGroup( id, allGroups );
						if ( group ) {
							rows.push( groupSelector.prepareMessageGroupRow( group ) );
						}
					} );
					return rows;
				} )
				.always( function () {
					groupSelector.$loader.addClass( 'hide' );
					groupSelector.$list.empty();
				} )
				.done( function ( rows ) {
					groupSelector.$list.append( rows );
				} );
		},

		/**
		 * Flattens a message group tree.
		 *
		 * @param {Array} messageGroups An array or data object.
		 * @param {Object} foundIDs The array in which the keys are IDs of message groups that were found already.
		 */
		flattenGroupList: function ( messageGroups, foundIDs ) {
			var messageGroupList;
			if ( messageGroups.groups ) {
				messageGroupList = messageGroups.groups;
			} else {
				messageGroupList = messageGroups;
			}

			for ( var i = 0; i < messageGroupList.length; i++ ) {
				// Avoid duplicate groups, and add the parent before subgroups
				if ( !foundIDs[ messageGroupList[ i ].id ] ) {
					this.flatGroupList.push( messageGroupList[ i ] );
					foundIDs[ messageGroupList[ i ].id ] = true;
				}

				// In case there are subgroups, add them recursively
				if ( messageGroupList[ i ].groups ) {
					this.flattenGroupList( messageGroupList[ i ].groups, foundIDs );
				}
			}
		},

		/**
		 * Search the message groups based on label or id.
		 * Label match is prefix match, while id match is exact match.
		 *
		 * @param {string} query
		 */
		filter: function ( query ) {
			var self = this;

			this.loadGroups().done( function ( groups ) {
				var foundGroups = [];

				if ( !self.flatGroupList ) {
					self.flatGroupList = [];
					var currentGroup = mw.translate.findGroup( self.parentGroupId, groups );
					if ( self.parentGroupId ) {
						currentGroup = currentGroup.groups;
					}
					self.flattenGroupList( currentGroup, {} );
				}

				// Optimization, assuming that people search the beginning
				// of the group name.
				var matcher = new RegExp( '\\b' + escapeRegex( query ), 'i' );

				for ( var index = 0; index < self.flatGroupList.length; index++ ) {
					if ( matcher.test( self.flatGroupList[ index ].label ) ||
						query === self.flatGroupList[ index ].id ) {
						foundGroups.push( self.flatGroupList[ index ] );
					}
				}

				self.$loader.addClass( 'hide' );
				self.$list.empty();
				self.addGroupRows( foundGroups );
			} );
		},

		/**
		 * Load message groups and relevant properties using the API.
		 *
		 * @return {jQuery.Promise}
		 */
		loadGroups: function () {
			if ( groupsLoader !== undefined ) {
				return groupsLoader;
			}

			var params = {
				action: 'query',
				meta: 'messagegroups',
				mgformat: 'tree',
				mgprop: 'id|label|icon',
				mgiconsize: '32',
				mglanguageFilter: this.options.language
			};

			groupsLoader = new mw.Api()
				.get( params )
				.then( function ( result ) {
					return result.query.messagegroups;
				} )
				.promise();

			return groupsLoader;
		},

		/**
		 * Add rows with message groups to the selector.
		 *
		 * @param {Array} groups Array of message group objects to add.
		 */
		addGroupRows: function ( groups ) {
			var groupSelector = this,
				$msgGroupRows = [];

			if ( !groups ) {
				return;
			}

			groups.forEach( function ( group ) {
				$msgGroupRows.push( groupSelector.prepareMessageGroupRow( group ) );
			} );

			if ( this.parentGroupId ) {
				var $parent = this.$list.find( '.tux-grouplist__item[data-msggroupid="' +
					this.parentGroupId + '"]' );

				if ( $parent.length ) {
					$parent.after( $msgGroupRows );
					return;
				}
			}

			this.$list.append( $msgGroupRows );
		},

		/**
		 * Prepare a message group row in the selector.
		 *
		 * @param {Object} messagegroup object.
		 * @return {Object} a jQuery object with the groups selector row (<div>).
		 */
		prepareMessageGroupRow: function ( messagegroup ) {
			var $row = $( '<div>' )
				.addClass( 'row tux-grouplist__item' )
				.attr( 'data-msggroupid', messagegroup.id )
				.data( 'msggroup', messagegroup );

			var $icon = $( '<div>' )
				.addClass( 'tux-grouplist__item__icon' )
				.addClass( 'one column' );

			var $statsbar = $( '<div>' ).languagestatsbar( {
				language: this.options.language,
				group: messagegroup.id
			} );

			var $label = $( '<div>' )
				.addClass( 'tux-grouplist__item__label' )
				.addClass( 'seven columns' )
				.append(
					$( '<span>' )
						// T130390: must be attr for IE/Edge.
						.attr( { dir: 'auto' } )
						.text( messagegroup.label ),
					$statsbar
				);

			var style = '';
			if ( messagegroup.icon && messagegroup.icon.raster ) {
				style += 'background-image: url(--);';
				style = style.replace( /--/g, messagegroup.icon.raster );
			}

			if ( messagegroup.icon && messagegroup.icon.vector ) {
				style += 'background-image: linear-gradient(transparent, transparent), url(--);';
				style = style.replace( /--/g, messagegroup.icon.vector );
			}

			if ( style !== '' ) {
				$icon.attr( 'style', style );
			}

			var $subGroupsLabel = $( [] );

			if ( messagegroup.groups && messagegroup.groups.length > 0 ) {
				$subGroupsLabel = $( '<div>' )
					.addClass( 'tux-grouplist__item__subgroups' )
					.addClass( 'four columns' )
					.text( mw.msg( 'translate-msggroupselector-view-subprojects',
						messagegroup.groups.length ) );
			}

			return $row.append( $icon, $label, $subGroupsLabel );
		},

		/**
		 * Check that a DOM event is supported by the $menu jQuery object.
		 *
		 * @param {string} eventName
		 * @return {boolean}
		 */
		eventSupported: function ( eventName ) {
			var $search = this.$menu.find( '.tux-groupselector__filter__search__input' ),
				isSupported = eventName in $search;

			if ( !isSupported ) {
				this.$element.setAttribute( eventName, 'return;' );
				isSupported = typeof this.$element[ eventName ] === 'function';
			}

			return isSupported;
		},

		/**
		 * Only shows message groups translatable to given target language
		 *
		 * @param {string} targetLanguage
		 */
		updateTargetLanguage: function ( targetLanguage ) {
			this.options.language = targetLanguage;
			groupsLoader = undefined;
			this.firstShow = true;
		}
	};

	/*
	 * msggroupselector PLUGIN DEFINITION
	 */

	$.fn.msggroupselector = function ( options, groups ) {
		return this.each( function () {
			var $this = $( this ),
				data = $this.data( 'msggroupselector' );

			if ( !data ) {
				$this.data( 'msggroupselector',
					( data = new TranslateMessageGroupSelector( this, options, groups ) )
				);
			}

			if ( typeof options === 'string' ) {
				data[ options ].call( $this );
			}
		} );
	};

	$.fn.msggroupselector.Constructor = TranslateMessageGroupSelector;

	$.fn.msggroupselector.defaults = {
		language: 'en',
		position: {
			my: 'left top',
			at: 'left-90 bottom+5'
		}
	};

	/*
	 * Private functions
	 */

	/**
	 * Escape the search query for regex match
	 *
	 * @param {string} value A search string to be escaped.
	 * @return {string} Escaped string that is safe to use for a search.
	 */
	function escapeRegex( value ) {
		return value.replace( /[-[\]{}()*+?.,\\^$|#\s]/g, '\\$&' );
	}

	delay = ( function () {
		var timer = 0;

		return function ( callback, milliseconds ) {
			clearTimeout( timer );
			timer = setTimeout( callback, milliseconds );
		};
	}() );
}() );
