import { type Editor, ModelText, ModelTextProxy, type ModelElement, type ModelRange, type Autoformat, inlineAutoformatEditing } from 'ckeditor5';

import { COMMANDS, ELEMENTS } from '../constants.js';
import { modelQueryElement, modelQueryElementsAll } from '../utils.js';

/**
 * CKEditor's autoformatting feature (basically find and replace) has two opinionated default modes:
 * block autoformatting, which replaces the entire line, and inline autoformatting,
 * which expects a section to be formatted (but, importantly, not removed) surrounded by
 * a pair of delimters which get removed.
 *
 * Neither of those are ideal for this case. We want to replace the matched text with a new element,
 * without deleting the entire line.
 *
 * However, inlineAutoformatEditing allows for passing in a custom callback to handle
 * regex matching, which also allows us to specify which sections to remove and
 * which sections pass on to the formatting callback. This method removes the entire
 * matched text, while passing the range of the numeric text on to the formatting callback.
 *
 * If 0 or more than 1 match is found, it returns empty ranges for both format and remove, which is a no-op.
 */
const regexMatchCallback = (
	editor: Editor,
	text: string
): {
  remove: Array<[number, number]>;
  format: Array<[number, number]>;
} => {
	const selectionStart = editor.model.document.selection.anchor;
	// get the text node containing the cursor's position, or the one ending at `the cursor's position
	const surroundingText = selectionStart && ( selectionStart.textNode || selectionStart.getShiftedBy( -1 ).textNode );

	if ( !selectionStart || !surroundingText ) {
		return {
			remove: [],
			format: []
		};
	}

	const results = text.matchAll( /\[\^([0-9]+)\]/g );

	for ( const result of results || [] ) {
		const removeStartIndex = text.indexOf( result[ 0 ] );
		const removeEndIndex = removeStartIndex + result[ 0 ].length;
		const textNodeOffset = selectionStart.parent.getChildStartOffset( surroundingText );

		// if the cursor isn't at the end of the range to be replaced, do nothing
		if ( textNodeOffset === null || selectionStart.offset !== textNodeOffset + removeEndIndex ) {
			continue;
		}
		const formatStartIndex = removeStartIndex + 2;
		const formatEndIndex = formatStartIndex + result[ 1 ].length;
		return {
			remove: [ [ removeStartIndex, removeEndIndex ] ],
			format: [ [ formatStartIndex, formatEndIndex ] ]
		};
	}
	return {
		remove: [],
		format: []
	};
};

/**
 * This callback takes in a range of text passed on by regexMatchCallback,
 * and attempts to insert a corresponding footnote reference at the current location.
 *
 * Footnotes only get inserted if the matching range is an integer between 1
 * and the number of existing footnotes + 1.
 */
const formatCallback = ( ranges: Array<ModelRange>, editor: Editor, rootElement: ModelElement ): boolean | undefined => {
	const command = editor.commands.get( COMMANDS.insertFootnote );
	if ( !command || !command.isEnabled ) {
		return;
	}
	const text = [ ...ranges[ 0 ].getItems() ][ 0 ];
	if ( !( text instanceof ModelTextProxy || text instanceof ModelText ) ) {
		return false;
	}
	const match = text.data.match( /[0-9]+/ );
	if ( !match ) {
		return false;
	}
	const footnoteIndex = parseInt( match[ 0 ] );
	const footnoteSection = modelQueryElement( editor, rootElement, element =>
		element.is( 'element', ELEMENTS.footnoteSection )
	);
	if ( !footnoteSection ) {
		if ( footnoteIndex !== 1 ) {
			return false;
		}
		editor.execute( COMMANDS.insertFootnote );
		return;
	}
	const footnoteCount = modelQueryElementsAll( editor, footnoteSection, element =>
		element.is( 'element', ELEMENTS.footnoteItem )
	).length;
	if ( footnoteIndex === footnoteCount + 1 ) {
		editor.execute( COMMANDS.insertFootnote );
		return;
	} else if ( footnoteIndex >= 1 && footnoteIndex <= footnoteCount ) {
		editor.execute( COMMANDS.insertFootnote, { footnoteIndex } );
		return;
	}
	return false;
};

/**
 * Adds functionality to support creating footnotes using markdown syntax, e.g. `[^1]`.
 */
export const addFootnoteAutoformatting = ( editor: Editor, rootElement: ModelElement ): void => {
	if ( editor.plugins.has( 'Autoformat' ) ) {
		const autoformatPluginInstance = editor.plugins.get( 'Autoformat' ) as Autoformat;
		inlineAutoformatEditing(
			editor,
			autoformatPluginInstance,
			text => regexMatchCallback( editor, text ),
			( _, ranges: Array<ModelRange> ) => formatCallback( ranges, editor, rootElement )
		);
	}
};
