/* global bp */

window.bp = window.bp || {};

( function( bp, $, undefined ) {
	var mentionsQueryCache = [],
		mentionsItem;

	bp.mentions       = bp.mentions || {};
	bp.mentions.users = window.bp.mentions.users || [];

	if ( typeof window.BP_Suggestions === 'object' ) {
		bp.mentions.users = window.BP_Suggestions.friends || bp.mentions.users;
	}

	/**
	 * Adds BuddyPress @mentions to form inputs.
	 *
	 * @param {array|object} options If array, becomes the suggestions' data source. If object, passed as config to $.atwho().
	 * @since 2.1.0
	 */
	$.fn.bp_mentions = function( options ) {
		if (this[0].is_mention_initialized) {
			return;
		}
		this[0].is_mention_initialized = true;

		if ( Array.isArray( options ) ) {
			options = { data: options };
		}

		/**
		 * Default options for at.js; see https://github.com/ichord/At.js/.
		 */
		var suggestionsDefaults = {
			delay:             200,
			hideWithoutSuffix: true,
			insertTpl:         '@{name}',	// not used because we are overloading tplEval
			limit:             10,
			startWithSpace:    false,
			suffix:            '',

			callbacks: {
				tplEval: function(tpl, map, event_type) {
					if ('onInsert' !== event_type) {
						return $.fn.atwho['default'].callbacks.tplEval(tpl, map, event_type);
					}
					let prefix, result,
						name = map['name'],
						user_id = map['user_id'],
						chr0 = String.fromCharCode(0),
						all_values;
					result = '@';
					if (!this.mention_map) {
						this.mention_map = {};
					}
					if (user_id in this.mention_map) {
						return this.mention_map[user_id];
					}
					prefix = '@';
					all_values = Object.values(this.mention_map);
					// this logic checks for duplicate names. Character 0 is prepended to distinguish the same display names
					while (true) {
						// end the @{name} with character 0, so that a "substring" display name (ie. username vs username123) won't be confused with the full name
						result = prefix + name + chr0;
						// already used
						if (all_values.indexOf(result) < 0) {
							this.mention_map[user_id] = result;
							return result;
						}
						prefix += chr0;
					}
					return '';
				},
				/**
				 * Custom filter to only match the start of spaced words.
				 * Based on the core/default one.
				 *
				 * @param {string} query
				 * @param {array} data
				 * @param {string} search_key
				 * @return {array}
				 * @since 2.1.0
				 */
				filter: function( query, data, search_key ) {
					var item, _i, _len, _results = [],
					regxp = new RegExp( '^' + query + '| ' + query, 'ig' ); // start of string, or preceded by a space.

					for ( _i = 0, _len = data.length; _i < _len; _i++ ) {
						item = data[ _i ];
						if ( item[ search_key ].toLowerCase().match( regxp ) ) {
							_results.push( item );
						}
					}

					return _results;
				},

				/**
				 * Removes some spaces around highlighted string and tweaks regex to allow spaces
				 * (to match display_name). Based on the core default.
				 *
				 * @param {unknown} li
				 * @param {string} query
				 * @return {string}
				 * @since 2.1.0
				 */
				highlighter: function( li, query ) {
					if ( ! query ) {
						return li;
					}

					var regexp = new RegExp( '>(\\s*|[\\w\\s]*)(' + this.at.replace( '+', '\\+') + '?' + query.replace( '+', '\\+' ) + ')([\\w ]*)\\s*<', 'ig' );
					return li.replace( regexp, function( str, $1, $2, $3 ) {
						return '>' + $1 + '<strong>' + $2 + '</strong>' + $3 + '<';
					});
				},

				/**
				 * Reposition the suggestion list dynamically.
				 *
				 * @param {unknown} offset
				 * @since 2.1.0
				 */
				before_reposition: function( offset ) {
					// get the iframe, if any, already applied with atwho.js library.
					var caret,
							line,
							iframeOffset,
							move,
							$view = $( '#atwho-ground-' + this.id + ' .atwho-view' ),
							$body = $( 'body' ),
							atwhoDataValue = this.$inputor.data( 'atwho' );

					if ( 'undefined' !== atwhoDataValue && 'undefined' !== atwhoDataValue.iframe && null !== atwhoDataValue.iframe ) {
						caret = this.$inputor.caret( 'offset', { iframe: atwhoDataValue.iframe } );
						// Caret.js no longer calculates iframe caret position from the window (it's now just within the iframe).
						// We need to get the iframe offset from the window and merge that into our object.
						iframeOffset = $( atwhoDataValue.iframe ).offset();
						if ( 'undefined' !== iframeOffset ) {
							caret.left += iframeOffset.left;
							caret.top  += iframeOffset.top;
						}
					} else {
						caret = this.$inputor.caret( 'offset' );
					}

					// If the caret is past horizontal half, then flip it, yo.
					if ( caret.left > ( $body.width() / 2 ) ) {
						$view.addClass( 'right' );
						move = caret.left - offset.left - this.view.$el.width();
					} else {
						$view.removeClass( 'right' );
						move = caret.left - offset.left + 1;
					}

					// If we're on a small screen, scroll to caret.
					if ( $body.width() <= 400 ) {
						$( document ).scrollTop( caret.top - 6 );
					}

					// New position is under the caret (never above) and positioned to follow.
					// Dynamic sizing based on the input area (remove 'px' from end).
					line = parseInt( this.$inputor.css( 'line-height' ).substr( 0, this.$inputor.css( 'line-height' ).length - 2 ), 10 );
					if ( !line || line < 5 ) { // Sanity check, and catch no line-height.
						line = 19;
					}

					offset.top   = caret.top + line;
					offset.left += move;
				},

				/**
				 * Override default behavior which inserts junk tags in the WordPress Visual editor.
				 *
				 * @param {unknown} $inputor Element which we're inserting content into.
				 * @param {string} content The content that will be inserted.
				 * @param {string} suffix Applied to the end of the content string.
				 * @return {string}
				 * @since 2.1.0
				 */
				inserting_wrapper: function( $inputor, content, suffix ) {
					return '' + content + suffix;
				}
			}
		},

		/**
		 * Default options for our @mentions; see https://github.com/ichord/At.js/.
		 */
		mentionsDefaults = {
			callbacks: {
				/**
				 * If there are no matches for the query in this.data, then query BuddyPress.
				 *
				 * @param {string} query Partial @mention to search for.
				 * @param {function} render_view Render page callback function.
				 * @since 2.1.0
				 * @since 3.0.0. Renamed from "remote_filter" for at.js v1.5.4 support.
				 */
				remoteFilter: function( query, render_view ) {
					var self = $( this ),
						params = {};

					mentionsItem = mentionsQueryCache[ query ];
					if ('sent' === mentionsItem) {
						return;
					}
					if ( typeof mentionsItem === 'object' ) {
						render_view( mentionsItem );
						return;
					}

					if ( self.xhr ) {
						self.xhr.abort();
					}
					mentionsQueryCache[ query ] = 'sent';

					params = { 'action': 'bp_get_suggestions', 'term': query, 'type': 'members' };

					var groupId = this.$inputor.data( 'suggestions-group-id' );
					if ( ( 'number' === typeof groupId || 'string' === typeof groupId ) && ! isNaN( groupId - parseFloat( groupId ) ) ) {
						params['group-id'] = parseInt( this.$inputor.data( 'suggestions-group-id' ), 10 );
					}

					self.xhr = $.getJSON( ajaxurl, params )
						/**
						 * Success callback for the @suggestions lookup.
						 *
						 * @param {object} response Details of users matching the query.
						 * @since 2.1.0
						 */
						.done(function( response ) {
							if ( ! response.success ) {
								mentionsQueryCache[ query ] = false;
								return;
							}

							var data = $.map( response.data,
								/**
								 * Create a composite index to determine ordering of results;
								 * nicename matches will appear on top.
								 *
								 * @param {array} suggestion A suggestion's original data.
								 * @return {array} A suggestion's new data.
								 * @since 2.1.0
								 */
								function( suggestion ) {
									suggestion.search = suggestion.search || suggestion.ID + ' ' + suggestion.name;
									return suggestion;
								}
							);

							mentionsQueryCache[ query ] = data;
							render_view( data );
						});
				}
			},

			data: $.map( options.data,
				/**
				 * Create a composite index to search against of nicename + display name.
				 * This will also determine ordering of results, so nicename matches will appear on top.
				 *
				 * @param {array} suggestion A suggestion's original data.
				 * @return {array} A suggestion's new data.
				 * @since 2.1.0
				 */
				function( suggestion ) {
					suggestion.search = suggestion.search || suggestion.ID + ' ' + suggestion.name;
					return suggestion;
				}
			),

			at:         '@',
			searchKey:  'search',
			displayTpl: '<li data-value="@${name}"><img src="${image}" alt="" /><span class="username">@${name}</span></li>'
		},

		opts = $.extend( true, {}, suggestionsDefaults, mentionsDefaults, options );
		this.on('inserted.atwho', function(e, $li, event, input_controller) {
			let $input_elem = input_controller.$inputor,
				mapping_name = 'mentions';
			// do not need to use a separate mention input when using tinyMCE
			if (options.input_elem) {
				return;
			}
			let $mapping_input = $input_elem.siblings('input[name="' + mapping_name + '"]');
			if ($mapping_input.length <= 0) {
				$mapping_input = $('<input type="hidden" name="' + mapping_name + '" />');
				$input_elem.after($mapping_input);
			} else {
				// merge existing value in the input with mention_map only when the first mention is added.
				if (1 === Object.keys(input_controller.mention_map).length) {
					let previous = JSON.parse($mapping_input.val());
					if (previous && 'object' === typeof previous) {
						for (let key in previous) {
							input_controller.mention_map[key] = previous[key];
						}
					}
				}
			}

			if (input_controller.mention_map) {
				$mapping_input.val(JSON.stringify(input_controller.mention_map));
			} else {
				$mapping_input.val('');
			}
		});
		return $.fn.atwho.call( this, opts );
	};

	$( document ).on('focus', '.bp-suggestions, #comments form textarea, .wp-editor-area', function() {
		// Activity/reply, post comments, dashboard post 'text' editor.
		$(this).bp_mentions({ data: bp.mentions.users, suffix: ' ' });
	});

	bp.mentions.tinyMCEinit = function(e) {
		if ( typeof window.tinyMCE === 'undefined' || window.tinyMCE.activeEditor === null || typeof window.tinyMCE.activeEditor === 'undefined' ) {
			return;
		} else {
			$( window.tinyMCE.activeEditor.contentDocument.activeElement )
				.atwho('setIframe', e.iframeElement)
				.bp_mentions({ data: bp.mentions.users, suffix: false, input_elem: $(e.targetElm) });	// the false suffix will default to a &nbsp; character
		}
	};
})( bp, jQuery );
