/*!
 * VisualEditor UserInterface MWSaveDialog class.
 *
 * @copyright 2011-2020 VisualEditor Team and others; see AUTHORS.txt
 * @license The MIT License (MIT); see LICENSE.txt
 */

/**
 * Dialog for saving MediaWiki pages.
 *
 * Note that most methods are not safe to call before the dialog has initialized, except where
 * noted otherwise.
 *
 * @class
 * @extends OO.ui.ProcessDialog
 *
 * @constructor
 * @param {Object} [config] Config options
 */
ve.ui.MWSaveDialog = function VeUiMwSaveDialog( config ) {
	// Parent constructor
	ve.ui.MWSaveDialog.super.call( this, config );

	// Properties
	this.editSummaryCodePointLimit = mw.config.get( 'wgCommentCodePointLimit' );
	this.restoring = false;
	this.messages = {};
	this.setupDeferred = ve.createDeferred();
	this.checkboxesByName = null;
	this.changedEditSummary = false;
	this.canReview = false;
	this.canPreview = false;
	this.hasDiff = false;
	this.diffElement = null;
	this.diffElementPromise = null;
	this.getDiffElementPromise = null;

	// Initialization
	this.$element.addClass( 've-ui-mwSaveDialog' );
};

/* Inheritance */

OO.inheritClass( ve.ui.MWSaveDialog, OO.ui.ProcessDialog );

/* Static Properties */

ve.ui.MWSaveDialog.static.name = 'mwSave';

ve.ui.MWSaveDialog.static.title =
	OO.ui.deferMsg( 'visualeditor-savedialog-title-save' );

ve.ui.MWSaveDialog.static.feedbackUrl = 'https://www.mediawiki.org/wiki/Talk:VisualEditor/Diffs';

ve.ui.MWSaveDialog.static.actions = [
	{
		action: 'save',
		// label will be set by config.saveButtonLabel
		flags: [ 'primary', 'progressive' ],
		modes: [ 'save', 'review', 'preview' ]
	},
	{
		label: OO.ui.deferMsg( 'visualeditor-savedialog-label-resume-editing' ),
		flags: [ 'safe', OO.ui.isMobile() ? 'back' : 'close' ],
		modes: [ 'save', 'conflict' ]
	},
	{
		action: 'review',
		label: OO.ui.deferMsg( 'visualeditor-savedialog-label-review' ),
		modes: [ 'save', 'preview' ]
	},
	{
		action: 'preview',
		label: OO.ui.deferMsg( 'showpreview' ),
		modes: [ 'save', 'review' ]
	},
	{
		action: 'approve',
		label: OO.ui.deferMsg( 'visualeditor-savedialog-label-review-good' ),
		flags: [ 'safe', 'back' ],
		modes: [ 'review', 'preview' ]
	},
	{
		action: 'resolve',
		label: OO.ui.deferMsg( 'visualeditor-savedialog-label-resolve-conflict' ),
		flags: [ 'primary', 'progressive' ],
		modes: 'conflict'
	},
	{
		action: 'report',
		label: OO.ui.deferMsg( 'visualeditor-savedialog-label-visual-diff-report' ),
		flags: [ 'progressive' ],
		modes: 'review',
		framed: false,
		icon: 'feedback',
		classes: [ 've-ui-mwSaveDialog-visualDiffFeedback' ],
		href: ve.ui.MWSaveDialog.static.feedbackUrl
	}
];

/* Events */

/**
 * @event save
 * @param {jQuery.Deferred} saveDeferred Deferred object to resolve/reject when the save
 *  succeeds/fails.
 * Emitted when the user clicks the save button
 */

/**
 * @event review
 * Emitted when the user clicks the review changes button
 */

/**
 * @event preview
 * Emitted when the user clicks the show preview button
 */

/**
 * @event resolve
 * Emitted when the user clicks the resolve conflict button
 */

/**
 * @event retry
 * Emitted when the user clicks the retry/continue save button after an error.
 */

/* Methods */

/**
 * Set review content and show review panel.
 *
 * @param {jQuery.Promise} wikitextDiffPromise Wikitext diff HTML promise
 * @param {jQuery.Promise} visualDiffGeneratorPromise Visual diff promise
 * @param {HTMLDocument} [baseDoc] Base document against which to normalise links when rendering visualDiff
 */
ve.ui.MWSaveDialog.prototype.setDiffAndReview = function ( wikitextDiffPromise, visualDiffGeneratorPromise, baseDoc ) {
	var dialog = this;

	this.clearDiff();

	function createDiffElement( visualDiff ) {
		var diffElement = new ve.ui.DiffElement( visualDiff );
		// The following classes are used here:
		// * mw-content-ltr
		// * mw-content-rtl
		diffElement.$document.addClass( 'mw-body-content mw-parser-output mw-content-' + visualDiff.newDoc.getDir() );
		ve.targetLinksToNewWindow( diffElement.$document[ 0 ] );
		// Run styles so links render with their appropriate classes
		ve.init.platform.linkCache.styleParsoidElements( diffElement.$document, baseDoc );
		mw.libs.ve.fixFragmentLinks( diffElement.$document[ 0 ], mw.Title.newFromText( ve.init.target.getPageName() ), 'mw-save-visualdiff-' );
		return diffElement;
	}

	// Visual diff
	this.$reviewVisualDiff.append( new OO.ui.ProgressBarWidget().$element );
	// Don't generate the DiffElement until the tab is switched to
	this.getDiffElementPromise = function () {
		return visualDiffGeneratorPromise.then( function ( visualDiffGenerator ) {
			return createDiffElement( visualDiffGenerator() );
		} );
	};

	this.baseDoc = baseDoc;

	// Wikitext diff
	this.$reviewWikitextDiff.append( new OO.ui.ProgressBarWidget().$element );
	wikitextDiffPromise.then( function ( wikitextDiff ) {
		if ( wikitextDiff ) {
			dialog.$reviewWikitextDiff.empty().append( wikitextDiff );
		} else {
			dialog.$reviewWikitextDiff.empty().append(
				$( '<div>' ).addClass( 've-ui-mwSaveDialog-no-changes' ).text( ve.msg( 'visualeditor-diff-no-changes' ) )
			);
		}
	}, function ( code, errorObject ) {
		var $errorMessage = ve.init.target.extractErrorMessages( errorObject );

		dialog.$reviewWikitextDiff.empty().append(
			new OO.ui.MessageWidget( {
				type: 'error',
				label: $errorMessage
			} ).$element
		);
	} ).always( function () {
		dialog.updateSize();
	} );

	this.hasDiff = true;
	this.popPending();
	this.swapPanel( 'review' );
};

/**
 * Set preview content and show preview panel.
 *
 * @param {HTMLDocument|jQuery} docOrMsg Document to preview, or error message
 * @param {HTMLDocument} [baseDoc] Base document against which to normalise links, if document provided
 */
ve.ui.MWSaveDialog.prototype.showPreview = function ( docOrMsg, baseDoc ) {
	var body, $heading, redirectMeta, deferred,
		$redirect = $(),
		categories = [],
		modules = [],
		dialog = this;

	if ( docOrMsg instanceof HTMLDocument ) {
		// Extract required modules for stylesheet tags (avoids re-loading styles)
		Array.prototype.forEach.call( docOrMsg.head.querySelectorAll( 'link[rel~=stylesheet]' ), function ( link ) {
			var uri = new mw.Uri( link.href );
			if ( uri.query.modules ) {
				modules = modules.concat( mw.libs.ve.expandModuleNames( uri.query.modules ) );
			}
		} );
		// Remove skin-specific modules (T187075 / T185284)
		modules = modules.filter( function ( module ) {
			return ( module.match( /^(skins|mediawiki\.skinning)\./ ) === null );
		} );
		mw.loader.using( modules );
		body = docOrMsg.body;
		// Take a snapshot of all categories
		Array.prototype.forEach.call( body.querySelectorAll( 'link[rel~="mw:PageProp/Category"]' ), function ( element ) {
			categories.push( ve.dm.nodeFactory.createFromElement( ve.dm.MWCategoryMetaItem.static.toDataElement( [ element ] ) ) );
		} );
		// Import body to current document, then resolve attributes against original document (parseDocument called #fixBase)
		document.adoptNode( body );

		// TODO: This code is very similar to ve.ui.PreviewElement+ve.ui.MWPreviewElement
		ve.resolveAttributes( body, docOrMsg, ve.dm.Converter.static.computedAttributes );

		$heading = $( '<h1>' ).addClass( 'firstHeading' );

		// Document title will only be set if wikitext contains {{DISPLAYTITLE}}
		if ( docOrMsg.title ) {
			// HACK: Parse title as it can contain basic wikitext (T122976)
			ve.init.target.getContentApi().post( {
				action: 'parse',
				title: ve.init.target.getPageName(),
				prop: 'displaytitle',
				text: '{{DISPLAYTITLE:' + docOrMsg.title + '}}\n'
			} ).then( function ( response ) {
				if ( ve.getProp( response, 'parse', 'displaytitle' ) ) {
					$heading.html( response.parse.displaytitle );
				}
			} );
		}

		// Redirect
		redirectMeta = body.querySelector( 'link[rel="mw:PageProp/redirect"]' );
		if ( redirectMeta ) {
			$redirect = ve.init.mw.ArticleTarget.static.buildRedirectMsg(
				mw.libs.ve.getTargetDataFromHref(
					redirectMeta.getAttribute( 'href' ),
					document
				).title
			);
		}

		this.$previewViewer.empty().append(
			// TODO: This won't work with formatted titles (T122976)
			$heading.text( docOrMsg.title || mw.Title.newFromText( ve.init.target.getPageName() ).getPrefixedText() ),
			$redirect,
			// The following classes are used here:
			// * mw-content-ltr
			// * mw-content-rtl
			$( '<div>' ).addClass( 'mw-content-' + mw.config.get( 'wgVisualEditor' ).pageLanguageDir ).append(
				body.childNodes
			)
		);

		ve.targetLinksToNewWindow( this.$previewViewer[ 0 ] );
		// Add styles so links render with their appropriate classes
		ve.init.platform.linkCache.styleParsoidElements( this.$previewViewer, baseDoc );
		mw.libs.ve.fixFragmentLinks( this.$previewViewer[ 0 ], mw.Title.newFromText( ve.init.target.getPageName() ), 'mw-save-preview-' );

		if ( categories.length ) {
			// If there are categories, we need to render them. This involves
			// a delay, since they might be hidden categories.
			deferred = ve.init.target.renderCategories( categories ).done( function ( $categories ) {
				dialog.$previewViewer.append( $categories );

				ve.targetLinksToNewWindow( $categories[ 0 ] );
				// Add styles so links render with their appropriate classes
				ve.init.platform.linkCache.styleParsoidElements( $categories, baseDoc );
			} );
		} else {
			deferred = ve.createDeferred().resolve();
		}
		deferred.done( function () {
			// Run hooks so other things can alter the document
			mw.hook( 'wikipage.content' ).fire( dialog.$previewViewer );
		} );
	} else if ( docOrMsg instanceof $ ) {
		this.$previewViewer.empty().append(
			$( '<em>' ).append( docOrMsg )
		);
	}

	this.popPending();
	this.swapPanel( 'preview' );
};

/**
 * @inheritdoc
 */
ve.ui.MWSaveDialog.prototype.pushPending = function () {
	this.getActions().setAbilities( { review: false, preview: false } );
	return ve.ui.MWSaveDialog.super.prototype.pushPending.call( this );
};

/**
 * @inheritdoc
 */
ve.ui.MWSaveDialog.prototype.popPending = function () {
	var ret = ve.ui.MWSaveDialog.super.prototype.popPending.call( this );
	if ( !this.isPending() ) {
		this.getActions().setAbilities( { review: true, preview: true } );
	}
	return ret;
};

/**
 * Clear the diff displayed in the review panel, if any.
 */
ve.ui.MWSaveDialog.prototype.clearDiff = function () {
	this.$reviewWikitextDiff.empty();
	this.$reviewVisualDiff.empty();
	this.$previewViewer.empty();
	this.hasDiff = false;
	this.diffElement = null;
	this.diffElementPromise = null;
	this.getDiffElementPromise = null;
};

/**
 * Swap state in the save dialog.
 *
 * @param {string} panel One of 'save', 'review' or 'conflict'
 * @param {boolean} [noFocus] Don't attempt to focus anything (e.g. while setting up)
 * @throws {Error} Unknown saveDialog panel
 */
ve.ui.MWSaveDialog.prototype.swapPanel = function ( panel, noFocus ) {
	var currentEditSummaryWikitext,
		mode = panel,
		size = 'medium',
		dialog = this,
		panelObj = dialog[ panel + 'Panel' ];

	if ( ( [ 'save', 'review', 'preview', 'conflict' ].indexOf( panel ) ) === -1 ) {
		throw new Error( 'Unknown saveDialog panel: ' + panel );
	}

	// Update the window title
	// The following messages are used here:
	// * visualeditor-savedialog-title-conflict
	// * visualeditor-savedialog-title-preview
	// * visualeditor-savedialog-title-review
	// * visualeditor-savedialog-title-save
	this.title.setLabel( ve.msg( 'visualeditor-savedialog-title-' + panel ) );

	// Reset save button if we disabled it for e.g. unrecoverable spam error
	this.actions.setAbilities( { save: true } );

	if ( !noFocus ) {
		// On panels without inputs, ensure the dialog is focused so events
		// are captured, e.g. 'Esc' to close
		this.$content[ 0 ].focus();
	}

	switch ( panel ) {
		case 'save':
			if ( !noFocus && this.panels.getCurrentItem() !== this.savePanel ) {
				// HACK: FF needs *another* defer
				setTimeout( function () {
					dialog.editSummaryInput.moveCursorToEnd();
				} );
			}
			break;
		case 'conflict':
			this.actions.setAbilities( { save: false } );
			break;
		case 'preview':
			size = 'full';
			this.previewPanel.$element[ 0 ].focus();
			this.previewPanel.$element.prepend( this.$previewEditSummaryContainer );
			break;
		case 'review':
			size = 'larger';
			this.reviewModeButtonSelect.$element.after( this.$previewEditSummaryContainer );
			setTimeout( function () {
				dialog.updateReviewMode();

				ve.track(
					'activity.' + dialog.constructor.static.name,
					{ action: 'review-initial-' + dialog.reviewModeButtonSelect.findSelectedItem().getData() }
				);
			} );
			break;
	}
	if ( panel === 'preview' || panel === 'review' ) {
		currentEditSummaryWikitext = this.editSummaryInput.getValue();
		if ( this.lastEditSummaryWikitext === undefined || this.lastEditSummaryWikitext !== currentEditSummaryWikitext ) {
			if ( this.editSummaryXhr ) {
				this.editSummaryXhr.abort();
			}
			this.lastEditSummaryWikitext = currentEditSummaryWikitext;
			this.$previewEditSummary.empty();

			if ( !currentEditSummaryWikitext || currentEditSummaryWikitext.trim() === '' ) {
				// Don't bother with an API request for an empty summary
				this.$previewEditSummary.text( ve.msg( 'visualeditor-savedialog-review-nosummary' ) );
			} else {
				this.$previewEditSummary.parent().removeClass( 'oo-ui-element-hidden' );
				this.$previewEditSummary.append( $.createSpinner() );
				this.editSummaryXhr = ve.init.target.getContentApi().post( {
					action: 'parse',
					title: ve.init.target.getPageName(),
					prop: '',
					summary: currentEditSummaryWikitext
				} ).done( function ( result ) {
					if ( result.parse.parsedsummary === '' ) {
						dialog.$previewEditSummary.parent().addClass( 'oo-ui-element-hidden' );
						dialog.$previewEditSummary.empty();
					} else {
						// Intentionally treated as HTML
						dialog.$previewEditSummary.html( ve.msg( 'parentheses', result.parse.parsedsummary ) );
						ve.targetLinksToNewWindow( dialog.$previewEditSummary[ 0 ] );
					}
				} ).fail( function () {
					dialog.$previewEditSummary.parent().addClass( 'oo-ui-element-hidden' );
					dialog.$previewEditSummary.empty();
				} ).always( function () {
					dialog.updateSize();
				} );
			}
		}
	}

	// Show the target panel
	this.panels.setItem( panelObj );
	this.setSize( size );

	// Set mode after setting size so that the footer is measured correctly
	this.actions.setMode( mode );

	// Only show preview in source mode
	this.actions.forEach( { actions: 'preview' }, function ( action ) {
		action.toggle( dialog.canPreview );
	} );

	// Diff API doesn't support section=new
	this.actions.forEach( { actions: 'review' }, function ( action ) {
		action.toggle( dialog.canReview );
	} );

	// Support: iOS
	// HACK: iOS Safari sometimes makes the entire panel completely disappear (T221289).
	// Rebuilding it makes it reappear.
	OO.ui.Element.static.reconsiderScrollbars( panelObj.$element[ 0 ] );

	mw.hook( 've.saveDialog.stateChanged' ).fire();
};

/**
 * Show a message in the save dialog.
 *
 * @param {string} name Message's unique name
 * @param {jQuery|string|Function|OO.ui.HtmlSnippet} label Message content. See OO.ui.mixin.LabelElement.
 * @param {string} config MessageWidget config. Defaults to an inline warning.
 * @return {jQuery.Promise} Promise which resolves when the message has been shown, rejects if no new message shown.
 */
ve.ui.MWSaveDialog.prototype.showMessage = function ( name, label, config ) {
	var messageWidget, promise;
	if ( !this.messages[ name ] ) {
		messageWidget = new OO.ui.MessageWidget( ve.extendObject( {
			classes: [ 've-ui-mwSaveDialog-message' ],
			label: label,
			inline: true,
			type: 'warning'
		}, config ) );
		this.$saveMessages.append( messageWidget.$element.css( 'display', 'none' ) );

		// FIXME: Use CSS transitions
		// eslint-disable-next-line no-jquery/no-slide
		promise = messageWidget.$element.slideDown( {
			duration: 250,
			progress: this.updateSize.bind( this )
		} ).promise();

		this.swapPanel( 'save' );

		// Can be hidden later by #clearMessage
		this.messages[ name ] = messageWidget.$element;

		return promise;
	}
	return ve.createDeferred().reject().promise();
};

/**
 * Remove a message from the save dialog.
 *
 * @param {string} name Message's unique name
 */
ve.ui.MWSaveDialog.prototype.clearMessage = function ( name ) {
	var $message = this.messages[ name ],
		dialog = this;
	if ( $message ) {
		// FIXME: Use CSS transitions
		// eslint-disable-next-line no-jquery/no-slide
		$message.slideUp( {
			duration: 250,
			progress: this.updateSize.bind( this )
		} ).promise().then( function () {
			$message.remove();
			dialog.updateSize();
		} );
		delete this.messages[ name ];
	}
};

/**
 * Remove all messages from the save dialog.
 */
ve.ui.MWSaveDialog.prototype.clearAllMessages = function () {
	this.$saveMessages.empty();
	this.messages = {};
};

/**
 * Reset the fields of the save dialog.
 */
ve.ui.MWSaveDialog.prototype.reset = function () {
	// Reset summary input
	this.editSummaryInput.setValue( '' );
	// Uncheck minoredit
	if ( this.checkboxesByName.wpMinoredit ) {
		this.checkboxesByName.wpMinoredit.setSelected( false );
	}
	this.clearDiff();
};

/**
 * Initialize MediaWiki page specific checkboxes.
 *
 * This method is safe to call even when the dialog hasn't been initialized yet.
 *
 * @param {OO.ui.FieldLayout[]} checkboxFields Checkbox fields
 */
ve.ui.MWSaveDialog.prototype.setupCheckboxes = function ( checkboxFields ) {
	var dialog = this;
	this.setupDeferred.done( function () {
		checkboxFields.forEach( function ( field ) {
			dialog.$saveCheckboxes.append( field.$element );
		} );
		dialog.updateOptionsBar();
	} );
};

/**
 * Change the edit summary prefilled in the save dialog.
 *
 * This method is safe to call even when the dialog hasn't been initialized yet.
 *
 * @param {string} summary Edit summary to prefill
 */
ve.ui.MWSaveDialog.prototype.setEditSummary = function ( summary ) {
	var dialog = this;
	this.setupDeferred.done( function () {
		dialog.editSummaryInput.setValue( summary );
	} );
};

/**
 * @inheritdoc
 */
ve.ui.MWSaveDialog.prototype.initialize = function () {
	var dialog = this,
		mwString = require( 'mediawiki.String' );

	// Parent method
	ve.ui.MWSaveDialog.super.prototype.initialize.call( this );

	// Properties
	this.panels = new OO.ui.StackLayout( { scrollable: false } );
	this.savePanel = new OO.ui.PanelLayout( {
		expanded: false,
		padded: true,
		classes: [ 've-ui-mwSaveDialog-savePanel' ]
	} );

	// Character counter in edit summary
	this.editSummaryCountLabel = new OO.ui.LabelWidget( {
		classes: [ 've-ui-mwSaveDialog-editSummary-count' ],
		label: '',
		title: ve.msg( 'visualeditor-editsummary-characters-remaining' )
	} );

	// Save panel
	this.$editSummaryLabel = $( '<div>' ).addClass( 've-ui-mwSaveDialog-summaryLabel' )
		.html( ve.init.platform.getParsedMessage( 'summary' ) );
	ve.targetLinksToNewWindow( this.$editSummaryLabel[ 0 ] );
	this.editSummaryInput = new ve.ui.MWEditSummaryWidget( {
		$overlay: this.$overlay,
		placeholder: ve.msg( 'visualeditor-editsummary' ),
		classes: [ 've-ui-mwSaveDialog-summary' ]
	} );
	// Show a warning if the user presses Enter
	this.editSummaryInput.on( 'enter', function () {
		dialog.showMessage(
			'keyboard-shortcut-submit',
			ve.msg(
				'visualeditor-savedialog-keyboard-shortcut-submit',
				new ve.ui.Trigger( ve.ui.commandHelpRegistry.lookup( 'dialogConfirm' ).shortcuts[ 0 ] ).getMessage()
			)
		).then( function () {
			// Restore focus after potential window resize
			dialog.editSummaryInput.focus();
		} );
	} );
	// Limit length, and display the remaining characters
	this.editSummaryInput.$input.codePointLimit( this.editSummaryCodePointLimit );
	this.editSummaryInput.on( 'change', function () {
		var remaining = dialog.editSummaryCodePointLimit - mwString.codePointLength( dialog.editSummaryInput.getValue() );
		// TODO: This looks a bit weird, there is no unit in the UI, just numbers.
		dialog.changedEditSummary = true;
		if ( remaining > 99 ) {
			dialog.editSummaryCountLabel.setLabel( '' );
		} else {
			dialog.editSummaryCountLabel.setLabel( mw.language.convertNumber( remaining ) );
		}

		dialog.updateOptionsBar();
	} );

	this.$saveCheckboxes = $( '<div>' ).addClass( 've-ui-mwSaveDialog-checkboxes' );
	this.$saveOptions = $( '<div>' ).addClass( 've-ui-mwSaveDialog-options' ).append(
		this.$saveCheckboxes,
		this.editSummaryCountLabel.$element
	);
	this.$license = $( '<p>' ).addClass( 've-ui-mwSaveDialog-license' )
		.html( ve.init.platform.getParsedMessage( 'copyrightwarning' ) );
	this.$saveMessages = $( '<div>' ).addClass( 've-ui-mwSaveDialog-messages' );
	this.$saveFoot = $( '<div>' ).addClass( 've-ui-mwSaveDialog-foot' ).append( this.$license );
	ve.targetLinksToNewWindow( this.$saveFoot[ 0 ] );
	this.savePanel.$element.append(
		this.$editSummaryLabel,
		this.editSummaryInput.$element,
		this.$saveOptions,
		this.$saveMessages,
		this.$saveFoot
	);

	this.updateOptionsBar();

	// Review panel
	this.reviewPanel = new OO.ui.PanelLayout( {
		expanded: false,
		padded: true
	} );

	this.$reviewVisualDiff = $( '<div>' ).addClass( 've-ui-mwSaveDialog-viewer' );
	this.$reviewWikitextDiff = $( '<div>' ).addClass( 've-ui-mwSaveDialog-viewer' );

	this.reviewModeButtonSelect = new OO.ui.ButtonSelectWidget( {
		items: [
			new OO.ui.ButtonOptionWidget( { data: 'visual', icon: 'eye', label: ve.msg( 'visualeditor-savedialog-review-visual' ) } ),
			new OO.ui.ButtonOptionWidget( { data: 'source', icon: 'wikiText', label: ve.msg( 'visualeditor-savedialog-review-wikitext' ) } )
		],
		classes: [ 've-ui-mwSaveDialog-reviewMode' ]
	} );
	this.reviewModeButtonSelect.connect( this, {
		choose: 'onReviewChoose',
		select: 'updateReviewMode'
	} );

	this.$previewEditSummary = $( '<span>' ).addClass( 've-ui-mwSaveDialog-summaryPreview' ).addClass( 'comment' );
	this.$previewEditSummaryContainer = $( '<div>' )
		.addClass( 'mw-summary-preview' )
		.text( ve.msg( 'summary-preview' ) )
		.append( $( '<br>' ), this.$previewEditSummary );
	this.$reviewActions = $( '<div>' ).addClass( 've-ui-mwSaveDialog-actions' );
	this.reviewPanel.$element.append(
		this.reviewModeButtonSelect.$element,
		this.$reviewVisualDiff,
		this.$reviewWikitextDiff,
		this.$reviewActions
	);

	// Preview panel
	this.previewPanel = new OO.ui.PanelLayout( {
		classes: [ 've-ui-mwSaveDialog-preview' ],
		expanded: false,
		padded: true
	} );
	this.$previewViewer = $( '<div>' ).addClass( 'mw-body-content mw-parser-output' );
	this.previewPanel.$element
		// Make focusable for keyboard accessible scrolling
		.prop( 'tabIndex', 0 )
		.append(
			$( '<div>' ).addClass( 'mw-content-container' ).append(
				$( '<div>' ).addClass( 'mw-body' ).append(
					this.$previewViewer
				)
			)
		);

	// Conflict panel
	this.conflictPanel = new OO.ui.PanelLayout( {
		expanded: false,
		padded: true
	} );
	this.$conflict = $( '<div>' ).addClass( 've-ui-mwSaveDialog-conflict' )
		.html( ve.init.platform.getParsedMessage( 'visualeditor-editconflict' ) );
	ve.targetLinksToNewWindow( this.$conflict[ 0 ] );
	this.conflictPanel.$element.append( this.$conflict );

	// Panel stack
	this.panels.addItems( [
		this.savePanel,
		this.reviewPanel,
		this.previewPanel,
		this.conflictPanel
	] );

	// Initialization
	this.$body.append( this.panels.$element );

	this.setupDeferred.resolve();
};

ve.ui.MWSaveDialog.prototype.updateOptionsBar = function () {
	var showOptions = !!this.editSummaryCountLabel.getLabel() || !this.$saveCheckboxes.is( ':empty' );
	if ( showOptions !== this.showOptions ) {
		this.savePanel.$element.toggleClass( 've-ui-mwSaveDialog-withOptions', showOptions );
		this.showOptions = showOptions;
		this.updateSize();
	}
};

/**
 * Update the current review mode
 *
 * @param  {OO.ui.ButtonOptionWidget} [button] The button clicked, or false if this is the initial setup
 */
ve.ui.MWSaveDialog.prototype.updateReviewMode = function () {
	var dialog = this,
		diffMode = this.reviewModeButtonSelect.findSelectedItem().getData(),
		surfaceMode = ve.init.target.getSurface().getMode(),
		isVisual = diffMode === 'visual';

	if ( !this.hasDiff ) {
		return;
	}

	// Config values used here:
	// * visualeditor-diffmode-visual
	// * visualeditor-diffmode-source
	ve.userConfig( 'visualeditor-diffmode-' + surfaceMode, diffMode );

	// Hack: cache report action so it is getable even when hidden (see T174497)
	if ( !this.report ) {
		this.report = this.getActions().get( { actions: 'report' } )[ 0 ];
	}

	this.$reviewVisualDiff.toggleClass( 'oo-ui-element-hidden', !isVisual );
	this.$reviewWikitextDiff.toggleClass( 'oo-ui-element-hidden', isVisual );
	if ( isVisual ) {
		this.report.toggle( true );
		if ( !this.diffElement ) {
			if ( !this.diffElementPromise ) {
				this.diffElementPromise = this.getDiffElementPromise().then( function ( diffElement ) {
					dialog.diffElement = diffElement;
					dialog.$reviewVisualDiff.empty().append( diffElement.$element );
					dialog.positionDiffElement();
				} );
			}
			return;
		}
		this.positionDiffElement();
	} else {
		this.report.toggle( false );
	}

	// Support: iOS
	// HACK: iOS Safari sometimes makes the entire panel completely disappear (T219680).
	// Rebuilding it makes it reappear.
	OO.ui.Element.static.reconsiderScrollbars( this.reviewPanel.$element[ 0 ] );

	this.updateSize();
};

/**
 * Update the current review mode
 *
 * @param {OO.ui.OptionWidget} item Item chosen
 */
ve.ui.MWSaveDialog.prototype.onReviewChoose = function ( item ) {
	ve.track( 'activity.' + this.constructor.static.name, { action: 'review-switch-' + item.getData() } );
};

/**
 * @inheritdoc
 */
ve.ui.MWSaveDialog.prototype.setDimensions = function () {
	// Parent method
	ve.ui.MWSaveDialog.parent.prototype.setDimensions.apply( this, arguments );

	if ( !this.positioning ) {
		this.positionDiffElement();
	}
};

/**
 * Re-position elements within the diff element
 *
 * Should be called whenever the diff element's container has changed width.
 */
ve.ui.MWSaveDialog.prototype.positionDiffElement = function () {
	var dialog = this;
	if ( this.panels.getCurrentItem() === this.reviewPanel ) {
		setTimeout( function () {
			dialog.withoutSizeTransitions( function () {
				// This is delayed, so check the visual diff is still visible
				if ( dialog.diffElement && dialog.isVisible() && dialog.reviewModeButtonSelect.findSelectedItem().getData() === 'visual' ) {
					dialog.diffElement.positionDescriptions();
					dialog.positioning = true;
					dialog.updateSize();
					dialog.positioning = false;
				}
			} );
		}, OO.ui.theme.getDialogTransitionDuration() );
	}
};

/**
 * @inheritdoc
 * @param {Object} [data]
 * @param {boolean} [data.canReview] User can review changes
 * @param {boolean} [data.canPreview] User can preview changes
 * @param {OO.ui.FieldLayout[]} [data.checkboxFields] Checkbox fields
 * @param {Object} [data.checkboxesByName] Checkbox widgets, indexed by name
 * @param {string} [data.sectionTitle] Section title, if in new section mode
 * @param {string} [data.editSummary] Edit summary
 * @param {string} [data.initialPanel='save'] Initial panel to show
 * @param {jQuery|string|OO.ui.HtmlSnippet|Function|null} [data.saveButtonLabel] Label for the save button
 */
ve.ui.MWSaveDialog.prototype.getSetupProcess = function ( data ) {
	return ve.ui.MWSaveDialog.super.prototype.getSetupProcess.call( this, data )
		.next( function () {
			var name,
				surfaceMode = ve.init.target.getSurface().getMode();

			this.canReview = !!data.canReview;
			this.canPreview = !!data.canPreview;
			this.setupCheckboxes( data.checkboxFields || [] );
			this.checkboxesByName = data.checkboxesByName || {};
			// HACK: Change layout when wpWatchlistExpiry is present to force wpWatchthis
			// onto a new line, hopefully with the expiry dropdown
			this.$saveCheckboxes.toggleClass( 've-ui-mwSaveDialog-checkboxes-withExpiry', !!this.checkboxesByName.wpWatchlistExpiry );
			// Toggle the watchlist-expiry dropdown's disabled state according to the
			// selected state of the watchthis checkbox.
			if ( this.checkboxesByName.wpWatchthis && this.checkboxesByName.wpWatchlistExpiry ) {
				// Set initial state to match the watchthis checkbox.
				this.checkboxesByName.wpWatchlistExpiry.setDisabled( !this.checkboxesByName.wpWatchthis.isSelected() );
				// Change state on every change of the watchthis checkbox.
				this.checkboxesByName.wpWatchthis.on( 'change', function ( enabled ) {
					this.checkboxesByName.wpWatchlistExpiry.setDisabled( !enabled );
				}.bind( this ) );
			}

			function trackCheckbox( n ) {
				ve.track( 'activity.mwSave', { action: 'checkbox-' + n } );
			}
			for ( name in this.checkboxesByName ) {
				this.checkboxesByName[ name ].$element.off( '.mwSave' ).on( 'click.mwSave', trackCheckbox.bind( this, name ) );
			}

			if ( data.sectionTitle ) {
				this.setEditSummary( ve.msg( 'newsectionsummary', data.sectionTitle ) );
				this.editSummaryInput.setDisabled( true );
			} else {
				this.editSummaryInput.setDisabled( false );
				if ( !this.changedEditSummary ) {
					this.setEditSummary( data.editSummary );
				}
			}

			// Config values used here:
			// * visualeditor-diffmode-visual
			// * visualeditor-diffmode-source
			this.reviewModeButtonSelect.selectItemByData(
				ve.userConfig( 'visualeditor-diffmode-' + surfaceMode ) || surfaceMode
			);

			// Old messages should not persist
			this.clearAllMessages();
			// Don't focus during setup to prevent scroll jumping (T153010)
			this.swapPanel( data.initialPanel || 'save', true );
			// Update save button label
			this.actions.forEach( { actions: 'save' }, function ( action ) {
				action.setLabel( data.saveButtonLabel );
			} );
		}, this );
};

/**
 * @inheritdoc
 */
ve.ui.MWSaveDialog.prototype.getReadyProcess = function ( data ) {
	return ve.ui.MWSaveDialog.super.prototype.getReadyProcess.call( this, data )
		.next( function () {
			// HACK: iOS Safari sometimes makes the entire panel completely disappear (T221289).
			// Rebuilding it makes it reappear.
			OO.ui.Element.static.reconsiderScrollbars( this.panels.getCurrentItem().$element[ 0 ] );

			// Support: Firefox
			// In Firefox, trying to focus a hidden input will throw an
			// exception. This would happen when opening the preview via
			// keyboard shortcut.
			if ( this.panels.getCurrentItem() === this.savePanel ) {
				// This includes a #focus call
				this.editSummaryInput.moveCursorToEnd();
			}
		}, this );
};

/**
 * @inheritdoc
 */
ve.ui.MWSaveDialog.prototype.getTeardownProcess = function ( data ) {
	return ve.ui.MWSaveDialog.super.prototype.getTeardownProcess.call( this, data )
		.next( function () {
			this.emit( 'close' );
			this.report = null;
		}, this );
};

/**
 * @inheritdoc
 */
ve.ui.MWSaveDialog.prototype.getActionProcess = function ( action ) {
	ve.track( 'activity.' + this.constructor.static.name, { action: 'dialog-' + ( action || 'abort' ) } );

	if ( action === 'save' ) {
		return new OO.ui.Process( function () {
			var saveDeferred = ve.createDeferred();
			this.clearMessage( 'keyboard-shortcut-submit' );
			this.emit( 'save', saveDeferred );
			return saveDeferred.promise();
		}, this );
	}
	if ( action === 'review' || action === 'preview' || action === 'resolve' ) {
		return new OO.ui.Process( function () {
			this.emit( action );
		}, this );
	}
	if ( action === 'approve' ) {
		return new OO.ui.Process( function () {
			this.swapPanel( 'save' );
		}, this );
	}
	if ( action === 'report' ) {
		return new OO.ui.Process( function () {
			window.open( this.constructor.static.feedbackUrl );
		}, this );
	}

	return ve.ui.MWSaveDialog.super.prototype.getActionProcess.call( this, action );
};

/**
 * @inheritdoc
 */
ve.ui.MWSaveDialog.prototype.getBodyHeight = function () {
	// Don't vary the height when the foot is made visible or not
	return this.panels.getCurrentItem().$element.outerHeight( true );
};

/**
 * Handle retry button click events.
 *
 * Hides errors and then tries again.
 */
ve.ui.MWSaveDialog.prototype.onRetryButtonClick = function () {
	this.emit( 'retry' );
	ve.ui.MWSaveDialog.super.prototype.onRetryButtonClick.apply( this, arguments );
};

/* Registration */

ve.ui.windowFactory.register( ve.ui.MWSaveDialog );
