/*!
 * VisualEditor UserInterface LinkAction class.
 *
 * @copyright See AUTHORS.txt
 */

/**
 * Link action.
 * This action transforms or inspects links (or potential links).
 *
 * @class
 * @extends ve.ui.Action
 * @constructor
 * @param {ve.ui.Surface} surface Surface to act on
 * @param {string} [source]
 */
ve.ui.LinkAction = function VeUiLinkAction() {
	// Parent constructor
	ve.ui.LinkAction.super.apply( this, arguments );
};

/* Inheritance */

OO.inheritClass( ve.ui.LinkAction, ve.ui.Action );

/* Static Properties */

ve.ui.LinkAction.static.name = 'link';

/**
 * @property {RegExp} RegExp matching an autolink + trailing space.
 *
 * @private
 */
ve.ui.LinkAction.static.autolinkRegExp = null; // Initialized below.

ve.ui.LinkAction.static.methods = [ 'autolinkUrl' ];

/* Methods */

/**
 * Autolink the selected URL (which may have trailing whitespace).
 *
 * @return {boolean}
 *   True if the selection is a valid URL and the autolink action was
 *   executed; otherwise false.
 */
ve.ui.LinkAction.prototype.autolinkUrl = function () {
	// Make sure we still have a real URL after trail removal, and not
	// a bare protocol (or no protocol at all, if we stripped the last
	// colon from the protocol)
	return this.autolink( ( linktext ) => ve.ui.LinkAction.static.autolinkRegExp.test( linktext ) );
};

/**
 * Autolink the selection, which may have trailing whitespace.
 *
 * @private
 * @param {Function} validateFunc
 *   A function used to validate the given linktext.
 * @param {string} validateFunc.linktext
 *   Linktext with trailing whitespace and punctuation stripped.
 * @param {boolean} validateFunc.return
 *   True iff the given linktext is valid.  If false, no linking will be done.
 * @param {Function} [txFunc]
 *   An optional function to create a transaction to perform the autolink.
 *   If not provided, a transaction will be created which applies the
 *   annotations returned by {@link ve.ui.LinkAction#getLinkAnnotation}.
 * @param {ve.dm.Document} txFunc.documentModel
 *   The document model to modify.
 * @param {ve.Range} txFunc.range
 *   The range to autolink.
 * @param {string} txFunc.linktext
 *   The text string to autolink.
 * @param {ve.dm.Transaction} txFunc.return
 *   The transaction to perform the autolink operation.
 * @return {boolean} Selection was valid and link action was executed.
 */
ve.ui.LinkAction.prototype.autolink = function ( validateFunc, txFunc ) {
	const surfaceModel = this.surface.getModel(),
		selection = surfaceModel.getSelection();

	if ( !( selection instanceof ve.dm.LinearSelection ) ) {
		return false;
	}

	function isLinkAnnotation( annotation ) {
		return /^link/.test( annotation.name );
	}

	let range = selection.getRange();
	const rangeEnd = range.end;

	const documentModel = surfaceModel.getDocument();
	let linktext = documentModel.data.getText( true, range );

	// Eliminate trailing whitespace.
	linktext = linktext.replace( /\s+$/, '' );

	// Eliminate trailing punctuation.
	linktext = linktext.replace( this.getTrailingPunctuation( linktext ), '' );

	// Validate the stripped text.
	if ( !validateFunc( linktext ) ) {
		// Don't autolink this.
		return false;
	}

	// Shrink range to match new linktext.
	range = range.truncate( linktext.length );

	// If there are word characters (but not punctuation) immediately past the range, don't autolink.
	// The user did something silly like type a link in the middle of a word.
	if (
		range.end + 1 < documentModel.data.getLength() &&
		/\w/.test( documentModel.data.getText( true, new ve.Range( range.end, range.end + 1 ) ) )
	) {
		return false;
	}

	// Check that none of the range has an existing link annotation.
	// Otherwise we could autolink an internal link, which would be ungood.
	for ( let i = range.start; i < range.end; i++ ) {
		if ( documentModel.data.getAnnotationsFromOffset( i ).containsMatching( isLinkAnnotation ) ) {
			// Don't autolink this.
			return false;
		}
	}

	// Make sure `undo` doesn't expose the selected linktext.
	surfaceModel.setLinearSelection( new ve.Range( rangeEnd ) );

	// Annotate the (previous) range.
	if ( txFunc ) {
		// TODO: Change this API so that 'txFunc' is given a surface fragment
		// as an argument, and uses the fragment to directly edit the document.
		surfaceModel.change( txFunc( documentModel, range, linktext ) );
	} else {
		surfaceModel.getLinearFragment( range, true ).annotateContent( 'set', this.getLinkAnnotation( linktext ) );
	}

	return true;
};

/**
 * Return an appropriate "trailing punctuation" set, which will
 * get stripped from possible autolinks.
 *
 * @param {string} candidate
 *   The candidate text.  Some users may not wish to include closing
 *   brackets/braces/parentheses in the stripped character class if an
 *   opening bracket/brace/parenthesis in present in the candidate link
 *   text.
 * @return {RegExp}
 *   A regular expression matching trailing punctuation which will be
 *   stripped from an autolink.
 */
ve.ui.LinkAction.prototype.getTrailingPunctuation = function () {
	return /[,;.:!?)\]}"'”’»]+$/;
};

/**
 * Return an appropriate annotation for the given link text.
 *
 * @param {string} linktext The link text to annotate.
 * @return {ve.dm.LinkAnnotation} The annotation to use.
 */
ve.ui.LinkAction.prototype.getLinkAnnotation = function ( linktext ) {
	return new ve.dm.LinkAnnotation( {
		type: 'link',
		attributes: {
			href: linktext
		}
	} );
};

/* Registration */

ve.ui.actionFactory.register( ve.ui.LinkAction );

// Delayed initialization (wait until ve.init.platform exists)
ve.init.Platform.static.initializedPromise.then( () => {
	ve.ui.LinkAction.static.autolinkRegExp =

		new RegExp(
			'\\b' + ve.init.platform.getUnanchoredExternalLinkUrlProtocolsRegExp().source + '\\S+$',
			'i'
		);

	ve.ui.sequenceRegistry.register(
		new ve.ui.Sequence(
			'autolinkUrl', 'autolinkUrl', ve.ui.LinkAction.static.autolinkRegExp, 0,
			{
				setSelection: true,
				delayed: true
			}
		)
	);
} );
