/**
 * External dependencies
 */
import removeAccents from 'remove-accents';

/**
 * Internal dependencies
 */
import type { WPCompleter } from './types';

type AutocompleteMatch = {
	completer: WPCompleter;
	filterValue: string;
};

type AutocompleteMatchOptions = {
	matchCount: number;
	isBackspacing: boolean;
	getTextAfterSelection: () => string;
	lastCompletion?: { name: string; value: string } | null;
};

export function getAutocompleteMatch(
	textContent: string,
	completers: WPCompleter[],
	options: AutocompleteMatchOptions
): AutocompleteMatch | null {
	const { matchCount, isBackspacing, getTextAfterSelection, lastCompletion } =
		options;

	if ( ! textContent ) {
		return null;
	}

	// Find the completer whose trigger prefix ends closest to the cursor
	// (rightmost end position). Comparing end positions instead of start
	// positions correctly resolves overlapping prefixes like "@" and "@@".
	let completer: WPCompleter | null = null;
	let triggerIndex = -1;
	let matchedEndIndex = -1;
	let matchedPrefixLength = 0;

	for ( const currentCompleter of completers ) {
		const currentIndex = textContent.lastIndexOf(
			currentCompleter.triggerPrefix
		);
		if ( currentIndex < 0 ) {
			continue;
		}
		const currentEndIndex =
			currentIndex + currentCompleter.triggerPrefix.length;
		if (
			currentEndIndex > matchedEndIndex ||
			( currentEndIndex === matchedEndIndex &&
				currentCompleter.triggerPrefix.length > matchedPrefixLength )
		) {
			completer = currentCompleter;
			triggerIndex = currentIndex;
			matchedEndIndex = currentEndIndex;
			matchedPrefixLength = currentCompleter.triggerPrefix.length;
		}
	}

	if ( ! completer ) {
		return null;
	}

	const { allowContext, triggerPrefix } = completer;

	const textWithoutTrigger = textContent.slice(
		triggerIndex + triggerPrefix.length
	);

	// Prevent matching with an extremely long string, which causes
	// the editor to slow-down significantly. This could happen, for
	// example, if `matchingWhileBackspacing` is true and one of the
	// "words" ends up being too long. Returning null here intentionally
	// resets the autocompleter state in the caller.
	if ( textWithoutTrigger.length > 50 ) {
		return null;
	}

	const mismatch = matchCount === 0;
	const wordsFromTrigger = textWithoutTrigger.split( /\s/ );

	// Allow matching when typing a trigger + the match string or when
	// clicking in an existing trigger word on the page.
	// E.g. "Some text @a" — "@a" is detected as a trigger word.
	const hasOneTriggerWord = wordsFromTrigger.length === 1;

	// Allow matching when backspacing near a trigger word (up to 3
	// words from the trigger character). This lets us recover from a
	// mismatch when backspacing while still imposing sane limits.
	// E.g. "Some text @marcelo sekkkk" — backspacing "kkkk" re-shows
	// the popup once the text matches again.
	const matchingWhileBackspacing =
		isBackspacing && wordsFromTrigger.length <= 3;

	if ( mismatch && ! ( matchingWhileBackspacing || hasOneTriggerWord ) ) {
		return null;
	}

	if (
		allowContext &&
		! allowContext(
			textContent.slice( 0, triggerIndex ),
			getTextAfterSelection()
		)
	) {
		return null;
	}

	if (
		/^\s/.test( textWithoutTrigger ) ||
		/\s\s+$/.test( textWithoutTrigger )
	) {
		return null;
	}

	// After a completion whose value starts with the trigger prefix
	// (e.g. @username), the trigger remains in the text and would
	// re-activate the autocompleter. Suppress the match when the
	// filter value still corresponds to the recently completed text.
	if (
		lastCompletion &&
		lastCompletion.name === completer.name &&
		textWithoutTrigger.trimEnd() === lastCompletion.value
	) {
		return null;
	}

	return {
		completer,
		filterValue: removeAccents( textWithoutTrigger ),
	};
}
