/**
 * EditCheckAction
 *
 * @class
 * @mixes OO.EventEmitter
 *
 * @param {Object} config Configuration options
 * @param {mw.editcheck.BaseEditCheck} config.check Check which created this action
 * @param {ve.dm.SurfaceFragment[]} config.fragments Affected fragments
 * @param {ve.dm.SurfaceFragment} [config.focusFragment] Fragment to focus
 * @param {jQuery|string|Function|OO.ui.HtmlSnippet} [config.title] Title
 * @param {jQuery|string|Function|OO.ui.HtmlSnippet} [config.message] Body message
 * @param {jQuery|string|Function|OO.ui.HtmlSnippet} [config.prompt] Prompt to show before choices
 * @param {string} [config.id] Optional unique identifier
 * @param {string} [config.icon] Optional icon name
 * @param {string} [config.type='warning'] Type of message (e.g., 'warning', 'error')
 * @param {boolean} [config.suggestion] Whether this is a suggestion
 * @param {Object[]} [config.choices] User choices
 */
mw.editcheck.EditCheckAction = function MWEditCheckAction( config ) {
	// Mixin constructor
	OO.EventEmitter.call( this );

	this.mode = config.mode || '';
	this.check = config.check;
	this.fragments = config.fragments;
	this.originalText = this.fragments.map( ( fragment ) => fragment.getText() );
	this.focusFragment = config.focusFragment;
	this.message = config.message;
	this.prompt = config.prompt;
	this.footer = config.footer;
	this.id = config.id;
	this.title = config.title;
	this.icon = config.icon;
	this.type = config.type || 'warning';
	this.choices = config.choices || config.check.constructor.static.choices;
	this.suggestion = config.suggestion;
	this.widget = null;
	this.stale = false;
};

/* Inheritance */

OO.mixinClass( mw.editcheck.EditCheckAction, OO.EventEmitter );

/* Events */

/**
 * Fired when the user selects an action (e.g., clicks a suggestion button).
 *
 * @event mw.editcheck.EditCheckAction#act
 * @param {jQuery.Promise} promise A promise that resolves when the action is complete
 */

/**
 * Fired when the action's stale state changes
 *
 * @event mw.editcheck.EditCheckAction#stale
 * @param {boolean} stale The check is stale
 */

/* Methods */

/**
 * Compare the start offsets of two actions.
 *
 * @param {mw.editcheck.EditCheckAction} a
 * @param {mw.editcheck.EditCheckAction} b
 * @return {number}
 */
mw.editcheck.EditCheckAction.static.compareStarts = function ( a, b ) {
	const aStart = a.getHighlightSelections()[ 0 ].getCoveringRange().start;
	const bStart = b.getHighlightSelections()[ 0 ].getCoveringRange().start;
	const difference = aStart - bStart;
	if ( difference === 0 ) {
		if ( a.check.takesFocus() ) {
			return -1;
		}
		if ( b.check.takesFocus() ) {
			return 1;
		}
	}
	return difference;
};

/**
 * Get the action's title
 *
 * @return {jQuery|string|Function|OO.ui.HtmlSnippet}
 */
mw.editcheck.EditCheckAction.prototype.getTitle = function () {
	return this.title || this.check.getTitle( this );
};

/**
 * Get the action's footer, if any
 *
 * @return {jQuery|string|Function|OO.ui.HtmlSnippet|undefined}
 */
mw.editcheck.EditCheckAction.prototype.getFooter = function () {
	return this.footer || this.check.getFooter( this );
};

/**
 * Get the prompt question for the current choices
 *
 * @return {jQuery|string|Function|OO.ui.HtmlSnippet|undefined}
 */
mw.editcheck.EditCheckAction.prototype.getPrompt = function () {
	return this.prompt || this.check.getPrompt( this );
};

/**
 * Get the available choices
 *
 * @return {Object[]}
 */
mw.editcheck.EditCheckAction.prototype.getChoices = function () {
	return this.choices;
};

/**
 * Get selections to highlight for this check
 *
 * @return {ve.dm.Selection[]}
 */
mw.editcheck.EditCheckAction.prototype.getHighlightSelections = function () {
	return this.fragments.map( ( fragment ) => fragment.getSelection() );
};

/**
 * Get the selection to focus for this check
 *
 * @return {ve.dm.Selection}
 */
mw.editcheck.EditCheckAction.prototype.getFocusSelection = function () {
	// TODO: Instead of fragments[0], create a fragment that covers all fragments?
	return ( this.focusFragment || this.fragments[ 0 ] ).getSelection();
};

/**
 * Get a description of the check
 *
 * @return {string}
 */
mw.editcheck.EditCheckAction.prototype.getDescription = function () {
	return this.message || this.check.getDescription( this );
};

/**
 * Get the type of this action (e.g., 'warning', 'error')
 *
 * @return {string}
 */
mw.editcheck.EditCheckAction.prototype.getType = function () {
	if ( this.suggestion ) {
		return 'success';
	}
	return this.type;
};

/**
 * Get the name of the check type
 *
 * @return {string} Check type name
 */
mw.editcheck.EditCheckAction.prototype.getName = function () {
	return this.check.getName();
};

/**
 * Whether this is a suggestion
 *
 * @return {boolean}
 */
mw.editcheck.EditCheckAction.prototype.isSuggestion = function () {
	return this.suggestion;
};

/**
 * Render as an EditCheckActionWidget
 *
 * @param {boolean} collapsed Start collapsed
 * @param {boolean} singleAction This is the only action shown
 * @param {ve.ui.Surface} surface Surface
 * @return {mw.editcheck.EditCheckActionWidget}
 */
mw.editcheck.EditCheckAction.prototype.render = function ( collapsed, singleAction, surface ) {
	this.widget = new mw.editcheck.EditCheckActionWidget( {
		type: this.getType(),
		icon: this.icon,
		name: this.getName(),
		label: this.getTitle(),
		message: this.getDescription(),
		footer: this.getFooter(),
		prompt: this.getPrompt(),
		choices: this.getChoices(),
		mode: this.mode,
		singleAction,
		suggestion: this.suggestion
	} );
	this.widget.connect( this, {
		actionClick: [ 'onActionClick', surface ]
	} );
	this.widget.toggleCollapse( collapsed );

	return this.widget;
};

/**
 * Set the mode used by the action widget
 *
 * @param {string} mode
 */
mw.editcheck.EditCheckAction.prototype.setMode = function ( mode ) {
	this.mode = mode;
	if ( this.widget ) {
		this.widget.setMode( mode );
	}
};

/**
 * Handle click events from an action button
 *
 * @param {ve.ui.Surface} surface Surface
 * @param {OO.ui.ActionWidget} actionWidget Clicked action widget
 * @fires mw.editcheck.EditCheckAction#act
 */
mw.editcheck.EditCheckAction.prototype.onActionClick = function ( surface, actionWidget ) {
	const promise = this.check.act( actionWidget.action, this, surface );
	this.emit( 'act', promise || ve.createDeferred().resolve().promise() );
	ve.track( 'activity.editCheck-' + this.getName(), {
		action: ( this.isSuggestion() ? 'suggestion-' : '' ) + 'action-' + ( actionWidget.getAction() || 'unknown' )
	} );
};

/**
 * Compare to another action
 *
 * @param {mw.editcheck.EditCheckAction} other Other action
 * @param {boolean} allowOverlaps Count overlaps rather than a perfect match
 * @return {boolean}
 */
mw.editcheck.EditCheckAction.prototype.equals = function ( other, allowOverlaps ) {
	if ( this.check.constructor !== other.check.constructor ) {
		return false;
	}
	if ( this.id || other.id ) {
		return this.id === other.id;
	}
	if ( this.fragments.length !== other.fragments.length ) {
		return false;
	}
	return this.fragments.every( ( fragment ) => {
		const selection = fragment.getSelection();
		return other.fragments.some( ( otherFragment ) => {
			if ( allowOverlaps ) {
				return otherFragment.getSelection().getCoveringRange().overlapsRange( selection.getCoveringRange() );
			} else {
				return otherFragment.getSelection().equals( selection );
			}
		} );
	} );
};

/**
 * Update the stale state of the action based on the text, or force a specific state
 *
 * @param {boolean} [forceStale] Force the action into a stale or not-stale state
 */
mw.editcheck.EditCheckAction.prototype.updateStale = function ( forceStale ) {
	const wasStale = this.isStale();
	if ( forceStale !== undefined ) {
		this.originalText = forceStale ? null : this.fragments.map( ( fragment ) => fragment.getText() );
	}
	this.stale = !this.originalText || !OO.compare(
		this.originalText,
		this.fragments.map( ( fragment ) => fragment.getText() )
	);
	if ( wasStale !== this.stale ) {
		this.emit( 'stale', this.stale );
	}
};

/**
 * Get the stale state of the action
 *
 * Users must call #updateStale first if they want to get the latest
 * state based on the current text.
 *
 * @return {boolean} The action is stale
 */
mw.editcheck.EditCheckAction.prototype.isStale = function () {
	return this.check.canBeStale() && this.stale;
};

/**
 * Method called by the controller when the action is removed from the action list
 */
mw.editcheck.EditCheckAction.prototype.discarded = function () {
	this.emit( 'discard' );
};

/**
 * Tag this action
 *
 * @param {string} tag
 */
mw.editcheck.EditCheckAction.prototype.tag = function ( tag ) {
	this.check.tag( tag, this );
};

/**
 * Untag this action
 *
 * @param {string} tag
 * @return {boolean} Whether anything was untagged
 */
mw.editcheck.EditCheckAction.prototype.untag = function ( tag ) {
	return this.check.untag( tag, this );
};

/**
 * Is this action tagged?
 *
 * @param {string} tag
 * @return {boolean}
 */
mw.editcheck.EditCheckAction.prototype.isTagged = function ( tag ) {
	if ( this.id ) {
		return this.check.isTaggedId( tag, this.id );
	} else {
		return this.fragments.some( ( fragment ) => this.check.isTaggedRange( tag, fragment.getSelection().getRange() ) );
	}
};

/**
 * Get unique tag name for this action
 *
 * @return {string}
 */
mw.editcheck.EditCheckAction.prototype.getTagName = function () {
	return this.check.constructor.static.name;
};