/* global autosize */

( function () {
	'use strict';

	/**
	 * Dictionary of classes that will be used by different types of notices
	 * TODO: Should probably review and rename these classes in the future to
	 * be more unique to the translate extension? Some themes use warning,
	 * error classes to style elements, and we do take help from these.
	 */
	var noticeTypes = {
		warning: 'warning',
		error: 'error',
		translateFail: 'translation-saving',
		diff: 'diff',
		fuzzy: 'fuzzy',
		getAllClasses: function () {
			var classes = [];

			for ( var prop in this ) {
				if ( typeof this[ prop ] === 'string' ) {
					classes.push( this[ prop ] );
				}
			}

			return classes;
		}
	};

	/**
	 * TranslateEditor Plugin
	 * Prepare the translation editor UI for a translation unit (message).
	 * This is mainly used with the messagetable plugin,
	 * but it is independent of messagetable.
	 * Example usage:
	 *
	 *     $( 'div.messageRow' ).translateeditor( {
	 *         message: messageObject // Mandatory message object
	 *     } );
	 *
	 * Assumptions: The jquery element to which translateeditor is applied will
	 * internally contain the editor's generated UI. So it is going to have the same width
	 * and inherited properies of the container.
	 * The container can mark the message item with class 'message'. This is not
	 * mandatory, but if found, when the editor is opened, the message item will be hidden
	 * and the editor will appear as if the message is replaced by the editor.
	 * See the UI of Translate messagetable for a demo.
	 *
	 * @private
	 * @param {HTMLElement} element
	 * @param {Object} options
	 * @param {Function} [options.beforeSave] Callback to call when translation is going to be saved.
	 * @param {Function} [options.onReady] Callback to call when the editor is ready.
	 * @param {Function} [options.onSave] Callback to call when translation has been saved.
	 * @param {Function} [options.onSkip] Callback to call when a message is skipped.
	 * @param {Object} options.message Object as returned by messagecollection api.
	 * @param {mw.translate.TranslationApiStorage} [options.storage]
	 */
	function TranslateEditor( element, options ) {
		this.$editTrigger = $( element );
		this.$editor = null;
		this.options = options;
		this.message = this.options.message;
		this.$messageItem = this.$editTrigger.find( '.message' );
		this.shown = false;
		this.dirty = false;
		this.saving = false;
		this.expanded = false;
		this.listen();
		this.storage = this.options.storage || new mw.translate.TranslationApiStorage();
		this.canDelete = mw.translate.canDelete();
		this.editFontClass = 'mw-editfont-' + mw.user.options.get( 'editfont' );
		this.delayValidation = delayer();
		this.validating = null;
	}

	TranslateEditor.prototype = {

		/**
		 * Initialize the plugin
		 *
		 * @internal
		 */
		init: function () {
			// In case we have already created the editor earlier,
			// don't add a new one. The existing one may have unsaved
			// changes.
			if ( this.$editor ) {
				return;
			}

			this.render();
			// onReady callback
			if ( this.options.onReady ) {
				this.options.onReady.call( this );
			}
		},

		/**
		 * Render the editor UI
		 *
		 * @private
		 */
		render: function () {
			this.$editor = $( '<div>' )
				.addClass( 'row tux-message-editor hide' )
				.append(
					this.prepareEditorColumn(),
					this.prepareInfoColumn()
				);

			this.expanded = false;
			this.$editTrigger.append( this.$editor );

			if ( this.message.properties && this.message.properties.status === 'fuzzy' ) {
				this.addNotice(
					mw.message( 'tux-editor-outdated-notice' ).escaped(),
					noticeTypes.fuzzy
				);
			}

			this.showTranslationHelpers();
		},

		/**
		 * Mark the message as unsaved because of edits, can be resumed later
		 *
		 * @private
		 * @param {string} [highlightClass] Class for background highlighting
		 */
		markUnsaved: function ( highlightClass ) {
			var $tuxListStatus = this.$editTrigger.find( '.tux-list-status' );

			highlightClass = highlightClass || 'tux-highlight';

			$tuxListStatus.children( '.tux-status-unsaved' ).remove();
			$tuxListStatus.children().addClass( 'hide' );
			// `highlightClass` documented above
			// eslint-disable-next-line mediawiki/class-doc
			$( '<span>' )
				.addClass( 'tux-status-unsaved ' + highlightClass )
				.text( mw.msg( 'tux-status-unsaved' ) )
				.appendTo( $tuxListStatus );
		},

		/**
		 * Mark the message as unsaved because of saving failure.
		 *
		 * @private
		 */
		markUnsavedFailure: function () {
			this.markUnsaved( 'tux-notice' );
		},

		/**
		 * Mark the message as no longer unsaved
		 *
		 * @internal
		 */
		markUnunsaved: function () {
			var $tuxListStatus = this.$editTrigger.find( '.tux-list-status' );

			$tuxListStatus.children( '.tux-status-unsaved' ).remove();
			$tuxListStatus.children().removeClass( 'hide' );

			this.dirty = false;
			mw.translate.dirty = false;
		},

		/**
		 * Mark the message as being saved
		 *
		 * @private
		 */
		markSaving: function () {
			var $tuxListStatus = this.$editTrigger.find( '.tux-list-status' );

			// Disable the save button
			this.$editor.find( '.tux-editor-save-button' )
				.prop( 'disabled', true );

			// Add a "Saving" indicator
			$tuxListStatus.empty();
			$( '<span>' )
				.addClass( 'tux-status-unsaved' )
				.text( mw.msg( 'tux-status-saving' ) )
				.appendTo( $tuxListStatus );
		},

		/**
		 * Mark the message as translated and successfully saved.
		 *
		 * @private
		 */
		markTranslated: function () {
			this.$editTrigger.find( '.tux-list-status' )
				.empty()
				.append( $( '<span>' )
					.addClass( 'tux-status-translated' )
					.text( mw.msg( 'tux-status-translated' ) )
				);

			this.$messageItem
				.removeClass( 'untranslated translated fuzzy proofread' )
				.addClass( 'translated' );

			this.dirty = false;

			if ( this.message.properties ) {
				$( '.tux-action-bar .tux-statsbar' ).trigger(
					'change',
					[ 'translated', this.message.properties.status ]
				);

				this.message.properties.status = 'translated';
				// TODO: Update any other statsbar for the same group in the page.
			}
		},

		/**
		 * Save the translation
		 *
		 * @private
		 */
		save: function () {
			var translateEditor = this;

			mw.hook( 'mw.translate.editor.beforeSubmit' ).fire( translateEditor.$editor );
			var translation = translateEditor.$editor.find( '.tux-textarea-translation' ).val();
			var editSummary = translateEditor.$editor.find( '.tux-input-editsummary' ).val() || '';

			translateEditor.saving = true;

			// beforeSave callback
			if ( translateEditor.options.beforeSave ) {
				translateEditor.options.beforeSave( translation );
			}

			// For responsiveness and efficiency,
			// immediately move to the next message.
			translateEditor.next();

			// Now the message definitely has a history,
			// so make sure the history menu item is shown
			translateEditor.$editor.find( '.message-tools-history' )
				.removeClass( 'hide' );

			// Show the delete menu item if the user can delete
			if ( this.canDelete ) {
				translateEditor.$editor.find( '.message-tools-delete' )
					.removeClass( 'hide' );
			}

			// Hide translation related to saving failure before saving again.
			translateEditor.removeNotices( noticeTypes.translateFail );
			this.storage.save(
				translateEditor.message.title,
				translation,
				editSummary
			).done( function ( response, xhr ) {
				var editResp = response.edit;
				if ( editResp.result === 'Success' ) {
					translateEditor.message.translation = translation;
					translateEditor.onSaveSuccess();
				} else {
					translateEditor.onSaveFail( [ mw.msg( 'tux-save-unknown-error' ) ] );
					mw.log( response, xhr );
				}
			} ).fail( function ( errorCode, response ) {
				if ( errorCode === 'http' || errorCode === 'ok-but-empty' ) {
					var api = new mw.Api();
					translateEditor.displayNotices(
						api.getErrorMessage( errorCode ),
						noticeTypes.error
					);
					return;
				}

				var errors = [];
				for ( var i = 0; i < response.errors.length; i++ ) {
					var error = response.errors[ i ];
					if ( error.code === 'assertuserfailed' ) {
						// eslint-disable-next-line no-alert
						alert( mw.msg( 'tux-session-expired' ) );
						break;
					} else if ( error.code === 'translate-validation-failed' ) {
						// Cancel the translation check API call to avoid extra
						// notices from appearing.
						if ( translateEditor.validating ) {
							translateEditor.validating.abort();
						} else {
							// Cancel the translation check API call that might be made
							// in the future.
							translateEditor.delayValidation( false );
						}

						translateEditor.removeNotices( [ noticeTypes.error, noticeTypes.warning ] );

						if ( error.data && error.data.validation ) {
							translateEditor.displayNotices(
								error.data.validation.warnings,
								noticeTypes.warning
							);
							translateEditor.displayNotices(
								error.data.validation.errors,
								noticeTypes.error
							);
						}
					}

					errors.push( error.html );
				}

				// This is placed at the bottom to ensure that the save error appears at the
				// top of the notices
				translateEditor.onSaveFail(
					errors.length ? errors : [ mw.msg( 'tux-save-unknown-error' ) ]
				);

				// Display all the notices whenever an error occurs.
				translateEditor.showMoreNotices();
			} );
		},

		/**
		 * Success handler for the translation saving.
		 *
		 * @private
		 */
		onSaveSuccess: function () {
			this.markTranslated();
			this.$editTrigger.find( '.tux-list-translation' )
				.text( this.message.translation );
			this.saving = false;

			// remove notices if any.
			this.removeNotices( noticeTypes.getAllClasses() );

			this.$editor.find( '.tux-notice' ).empty();
			this.$editor.find( '.tux-more-notices' )
				.addClass( 'hide' )
				.empty();

			$( '.tux-editor-clear-translated' )
				.removeClass( 'hide' )
				.prop( 'disabled', false );

			this.$editor.find( '.tux-input-editsummary' )
				.val( '' )
				.prop( 'disabled', true );

			// Save callback
			if ( this.options.onSave ) {
				this.options.onSave( this.message.translation );
			}

			mw.translate.dirty = false;
			mw.hook( 'mw.translate.editor.afterSubmit' ).fire( this.$editor );

			if ( mw.track ) {
				mw.track( 'ext.translate.event.translation', this.message );
			}
		},

		/**
		 * Marks that there was a problem saving a translation.
		 *
		 * @private
		 * @param {string[]} errors Array of HTML notices to display.
		 */
		onSaveFail: function ( errors ) {
			var $error;
			if ( errors.length === 1 ) {
				$error = $( $.parseHTML( errors[ 0 ] ) );
			} else {
				var $errorList = $( '<ul>' );
				for ( var i = 0; i < errors.length; i++ ) {
					$errorList.append( $( '<li>' ).html( errors[ i ] ) );
				}
				$error = $errorList;
			}

			this.addNotice(
				mw.message( 'tux-editor-save-failed', $error, errors.length ).parse(),
				noticeTypes.translateFail
			);
			this.saving = false;
			this.markUnsavedFailure();

			// Enable the save button again
			this.$editor.find( '.tux-editor-save-button' ).prop( 'disabled', false );
		},

		/**
		 * Skip the current message. Record it to mark as hard.
		 *
		 * @private
		 */
		skip: function () {
			// @TODO devise good algorithm for identifying hard to translate messages
		},

		/**
		 * Jump to the next translation editor row.
		 *
		 * @private
		 */
		next: function () {
			var $next = this.$editTrigger.next( '.tux-message' );

			// Determine the next message to show. The immediate next one maybe hidden
			// for example in case of filtering
			while ( $next.length && $next.hasClass( 'hide' ) ) {
				$next = $next.next( '.tux-message' );
			}

			// If this is the last message, just hide it
			if ( !$next.length ) {
				this.hide();

				return;
			}

			$next.data( 'translateeditor' ).show();

			// Scroll the page a little bit up, slowly.
			if ( $( document ).height() -
				( document.documentElement.clientHeight + window.scrollY ) > 0
			) {
				var scrollTop = window.scrollY + $next.get( 0 ).getBoundingClientRect().top - 85;
				window.scrollTo( {
					top: scrollTop,
					left: 0,
					behavior: 'smooth'
				} );
			}
		},

		/**
		 * Creates a menu element for the message tools.
		 *
		 * @private
		 * @param {string} className Used as the element's CSS class
		 * @param {Object} query Used as the query in the mw.Uri object
		 * @param {string} message The message of the label of the menu item
		 * @return {jQuery} The new menu item element
		 */
		createMessageToolsItem: function ( className, query, message ) {
			var uri = new mw.Uri();

			uri.path = mw.config.get( 'wgScript' );
			uri.query = query;

			return $( '<li>' )
				.addClass( className )
				.append( $( '<a>' )
					.attr( {
						href: uri.toString(),
						target: '_blank'
					} )
					.text( mw.msg( message ) )
				);
		},

		/**
		 * Creates an element with a dropdown menu including
		 * tools for the translators.
		 *
		 * @private
		 * @return {jQuery} The new message tools menu element
		 */
		createMessageTools: function () {
			var $editItem = this.createMessageToolsItem(
				'message-tools-edit',
				{
					title: this.message.title,
					action: 'edit'
				},
				'tux-editor-message-tools-show-editor'
			);

			if ( !mw.translate.canTranslate() ) {
				$editItem.addClass( 'hide' );
			}

			var $historyItem = this.createMessageToolsItem(
				'message-tools-history',
				{
					title: this.message.title,
					action: 'history'
				},
				'tux-editor-message-tools-history'
			);

			var $deleteItem = this.createMessageToolsItem(
				'message-tools-delete',
				{
					title: this.message.title,
					action: 'delete'
				},
				'tux-editor-message-tools-delete'
			);

			// Hide these links if the translation doesn't actually exist.
			// They will be shown when a translation will be created.
			if ( this.message.translation === null ) {
				$historyItem.addClass( 'hide' );
				$deleteItem.addClass( 'hide' );
			} else if ( !this.canDelete ) {
				$deleteItem.addClass( 'hide' );
			}

			// A link to Special:Translations,
			// with translations of this message to other languages
			var $translationsItem = this.createMessageToolsItem(
				'message-tools-translations',
				{
					title: 'Special:Translations',
					message: this.message.title
				},
				'tux-editor-message-tools-translations'
			);

			var $linkToThisItem = this.createMessageToolsItem(
				'message-tools-linktothis',
				{
					title: 'Special:Translate',
					showMessage: this.message.key,
					group: this.message.primaryGroup,
					language: this.message.targetLanguage
				},
				'tux-editor-message-tools-linktothis'
			);

			return $( '<ul>' )
				.addClass( 'tux-dropdown-menu tux-message-tools-menu hide' )
				.append( $editItem, $historyItem, $deleteItem, $translationsItem, $linkToThisItem );
		},

		/**
		 * @private
		 * @return {jQuery}
		 */
		prepareEditorColumn: function () {
			var translateEditor = this,
				$discardChangesButton = $( [] ),
				$saveButton = $( [] ),
				$messageTools = translateEditor.createMessageTools(),
				canTranslate = mw.translate.canTranslate();

			var $editorColumn = $( '<div>' )
				.addClass( 'seven columns editcolumn' );

			var $messageKeyLabel = $( '<div>' )
				.addClass( 'ten columns messagekey' )
				.text( this.message.title )
				.append(
					$( '<span>' ).addClass( 'caret' ),
					$messageTools
				)
				.on( 'click', function ( e ) {
					$messageTools.toggleClass( 'hide' );
					e.stopPropagation();
				} );

			var $closeIcon = $( '<span>' )
				.addClass( 'one column close' )
				.attr( 'title', mw.msg( 'tux-editor-close-tooltip' ) )
				.on( 'click', function ( e ) {
					translateEditor.hide();
					e.stopPropagation();
				} );

			var $infoToggleIcon = $( '<span>' )
				// Initially the editor column is contracted,
				// so show the expand button first
				.addClass( 'one column editor-info-toggle editor-expand' )
				.attr( 'title', mw.msg( 'tux-editor-expand-tooltip' ) )
				.on( 'click', function ( e ) {
					translateEditor.infoToggle( $( this ) );
					e.stopPropagation();
				} );

			var $layoutActions = $( '<div>' )
				.addClass( 'two columns layout-actions' )
				.append( $closeIcon, $infoToggleIcon );

			$editorColumn.append( $( '<div>' )
				.addClass( 'row tux-editor-titletools' )
				.append( $messageKeyLabel, $layoutActions )
			);

			var $messageList = $( '.tux-messagelist' );
			var originalTranslation = this.message.translation;
			var sourceString = this.message.definition;
			// The following classes are used here:
			// * mw-editfont-serif
			// * mw-editfont-sans-serif
			// * mw-editfont-monospace
			var $sourceString = $( '<span>' )
				.addClass( 'twelve columns sourcemessage ' + this.editFontClass )
				.attr( {
					lang: $messageList.data( 'sourcelangcode' ),
					dir: $messageList.data( 'sourcelangdir' )
				} )
				.text( sourceString );

			// Adjust the font size for the message string based on the length
			if ( sourceString.length > 100 && sourceString.length < 200 ) {
				$sourceString.addClass( 'long' );
			}

			if ( sourceString.length > 200 ) {
				$sourceString.addClass( 'longer' );
			}

			var $copyOriginalButton = null;
			if ( window.navigator.clipboard ) {
				$copyOriginalButton = $( '<button>' )
					.addClass( 'tux-editor-copy-original-button' )
					.prop( 'title', mw.msg( 'tux-editor-copy-original-button-label' ) )
					.on( 'click', function () {
						window.navigator.clipboard.writeText( sourceString );
						var $self = $( this );
						$self
							.addClass( 'copied' )
							.prop( {
								disabled: true,
								title: mw.msg( 'tux-editor-copied-original-button-label' )
							} );
						setTimeout( function () {
							$self
								.prop( {
									disabled: false,
									title: mw.msg( 'tux-editor-copy-original-button-label' )
								} )
								.removeClass( 'copied' );
						}, 2000 );
					} );
			}

			$editorColumn.append( $( '<div>' )
				.addClass( 'row tux-editor-sourcemessage-container' )
				.append( $sourceString, $copyOriginalButton )
			);

			var $notices = $( '<div>' )
				.addClass( 'tux-notice hide' );

			var $moreNoticesTab = $( '<div>' )
				.addClass( 'tux-more-notices hide' )
				.on( 'click', function () {
					var $this = $( this ),
						$moreNotices = $notices.children(),
						lastNoticeIndex = $moreNotices.length - 1;

					// If the notice list is not open, only one notice is shown
					if ( $this.hasClass( 'open' ) ) {
						$moreNotices.each( function ( index, element ) {
							// The first element must always be shown
							if ( index ) {
								$( element ).addClass( 'hide' );
							}
						} );

						$this
							.removeClass( 'open' )
							.text( mw.msg( 'tux-notices-more', lastNoticeIndex ) );
					} else {
						$moreNotices.each( function ( index, element ) {
							// The first element must always be shown
							if ( index ) {
								$( element ).removeClass( 'hide' );
							}
						} );

						$this
							.addClass( 'open' )
							.text( mw.msg( 'tux-notices-hide' ) );
					}

					translateEditor.toggleMoreButtonClass();
				} );

			var $textarea = this.getTranslationEditor( this.message.targetLanguage );

			// Shortcuts for various insertable things
			$textarea.on( 'keyup keydown', function ( e ) {
				var index, $info, direction;

				if ( e.type === 'keydown' && e.altKey === true ) {
					// Up and down arrows
					if ( e.keyCode === 38 || e.keyCode === 40 ) {
						direction = e.keyCode === 40 ? 1 : -1;
						$info = translateEditor.$editor.find( '.infocolumn' );
						$info.scrollTop( $info.scrollTop() + 100 * direction );
						translateEditor.showShortcuts();
					}
				}

				// Move zero to last
				index = e.keyCode - 49;
				if ( index === -1 ) {
					index = 9;
				}

				// 0..9 ~ 48..57
				if (
					e.type === 'keydown' &&
					e.altKey === true &&
					e.ctrlKey === false &&
					e.shiftKey === false &&
					index >= 0 && index < 10
				) {
					e.preventDefault();
					e.stopPropagation();
					translateEditor.$editor.find( '.shortcut-activated:visible' ).eq( index ).trigger( 'click' );
					// Update numbers and locations after trigger should be completed
					window.setTimeout( function () {
						translateEditor.showShortcuts();
					}, 100 );
				}

				if ( e.which === 18 && e.type === 'keyup' ) {
					translateEditor.hideShortcuts();
				} else if ( e.which === 18 && e.type === 'keydown' ) {
					translateEditor.showShortcuts();
				}
			} );

			$textarea.on( 'input', function () {
				var $pasteSourceButton = translateEditor.$editor.find( '.tux-editor-paste-original-button' ),
					original = translateEditor.message.translation || '',
					current = $textarea.val() || '';

				if ( original !== '' ) {
					$discardChangesButton.removeClass( 'hide' );
				}

				/* Avoid Unsaved marking when translated message is not changed in content.
				 * - translateEditor.dirty: internal book keeping
				 * - mw.translate.dirty: "you have unchanged edits" notice
				 */
				if ( original === current ) {
					translateEditor.markUnunsaved();
				} else {
					translateEditor.dirty = true;
					mw.translate.dirty = true;
				}

				translateEditor.makeSaveButtonJustSave( $saveButton );

				// When there is content in the editor enable the button.
				// But do not enable when some saving is not finished yet.
				var enabled = current.trim() && !translateEditor.saving;
				$saveButton.prop( 'disabled', !enabled );
				$pasteSourceButton.toggleClass( 'hide', enabled );

				translateEditor.resizeInsertables( $textarea );

				translateEditor.delayValidation( function () {
					translateEditor.validateTranslation();
				}, 1000 );
			} );

			var $noticesBlock = $( '<div>' )
				.addClass( 'tux-notices-block' )
				.append( $moreNoticesTab, $notices );

			var $editAreaBlock = $( '<div>' )
				.addClass( 'row tux-editor-editarea-block' )
				.append( $( '<div>' )
					.addClass( 'editarea twelve columns' )
					.append( $noticesBlock, $textarea )
				);

			$editorColumn.append( $editAreaBlock );

			var $editingButtonBlock, $editSummaryBlock, $requestRight, $skipButton;
			if ( canTranslate ) {
				var $pasteOriginalButton = $( '<button>' )
					.addClass( 'tux-editor-paste-original-button' )
					.text( mw.msg( 'tux-editor-paste-original-button-label' ) )
					.on( 'click', function () {
						$textarea
							.trigger( 'focus' )
							.val( sourceString )
							.trigger( 'input' );

						$pasteOriginalButton.addClass( 'hide' );
					} );

				var $editSummary = $( '<input>' )
					.addClass( 'tux-input-editsummary' )
					.attr( {
						maxlength: 255,
						disabled: true,
						placeholder: mw.msg( 'tux-editor-editsummary-placeholder' )
					} )
					.val( '' );

				// Enable edit summary if there was a change to translation area
				// or disable if there is no text in translation area
				$textarea.on( 'input', function () {
					if ( $editSummary.prop( 'disabled' ) ) {
						$editSummary.prop( 'disabled', false );
					}
					if ( $textarea.val().trim() === '' ) {
						$editSummary.prop( 'disabled', true );
					}
				} ).on( 'keydown', function ( e ) {
					if ( !e.ctrlKey || e.keyCode !== 13 ) {
						return;
					}

					if ( !$saveButton.is( ':disabled' ) ) {
						$saveButton.trigger( 'click' );
						return;
					}
					$skipButton.trigger( 'click' );
				} );

				// Make the Ctrl+Enter shortcut work in the edit summary field
				$editSummary.on( 'keydown', function ( e ) {
					if ( !e.ctrlKey || e.keyCode !== 13 ) {
						return;
					}

					$saveButton.trigger( 'click' );
				} );

				if ( originalTranslation !== null ) {
					$discardChangesButton = $( '<button>' )
						.addClass( 'tux-editor-discard-changes-button hide' ) // Initially hidden
						.text( mw.msg( 'tux-editor-discard-changes-button-label' ) )
						.on( 'click', function () {
							// Restore the translation
							$textarea
								.trigger( 'focus' )
								.val( originalTranslation );

							// and go back to hiding.
							$discardChangesButton.addClass( 'hide' );

							// There's nothing new to save...
							$editSummary.val( '' ).prop( 'disabled', true );
							$saveButton.prop( 'disabled', true );
							// ...unless there is other action
							translateEditor.makeSaveButtonContextSensitive( $saveButton );

							translateEditor.markUnunsaved();
							translateEditor.resizeInsertables( $textarea );
						} );
				}

				if ( this.message.translation ) {
					$pasteOriginalButton.addClass( 'hide' );
				}

				$editingButtonBlock = $( '<div>' )
					.addClass( 'twelve columns tux-editor-insert-buttons' )
					.append(
						$pasteOriginalButton,
						$discardChangesButton
					);

				$editSummaryBlock = $( '<div>' )
					.addClass( 'row tux-editor-editsummary-block' )
					.append(
						$( '<div>' )
							.addClass( 'twelve columns' )
							.append( $editSummary )
					);

				$requestRight = $( [] );

				$saveButton = $( '<button>' )
					.prop( 'disabled', true )
					.addClass( 'tux-editor-save-button mw-ui-button mw-ui-progressive' )
					.text( mw.msg( 'tux-editor-save-button-label' ) )
					.on( 'click', function ( e ) {
						translateEditor.save();
						e.stopPropagation();
					} );

				this.makeSaveButtonContextSensitive( $saveButton, this.$messageItem );
			} else {
				$editingButtonBlock = $( [] );

				$editSummaryBlock = $( [] );

				$requestRight = $( '<span>' )
					.addClass( 'tux-editor-request-right' )
					.text( mw.msg( 'translate-edit-nopermission' ) );
				// Make sure wgTranslatePermissionUrl setting is not 'false'
				if ( mw.config.get( 'wgTranslatePermissionUrl' ) !== false ) {
					$requestRight
						.append( $( '<a>' )
							.text( mw.msg( 'translate-edit-askpermission' ) )
							.addClass( 'tux-editor-ask-permission' )
							.attr( {
								href: mw.util.getUrl(
									mw.config.get( 'wgTranslateUseSandbox' ) ?
										'Special:TranslationStash' :
										mw.config.get( 'wgTranslatePermissionUrl' )
								)
							} )
						);
				}
				// Disable the text area if user has no translation rights.
				// Use readonly to allow copy-pasting (except for placeholders)
				$textarea.prop( 'readonly', true );

				$saveButton = $( [] );
			}

			$skipButton = $( '<button>' )
				.addClass( 'tux-editor-skip-button mw-ui-button mw-ui-quiet' )
				.text( mw.msg( 'tux-editor-skip-button-label' ) )
				.on( 'click', function ( e ) {
					translateEditor.skip();
					translateEditor.next();

					if ( translateEditor.options.onSkip ) {
						translateEditor.options.onSkip.call( translateEditor );
					}

					e.stopPropagation();
				} );

			// This appears instead of "Skip" on the last message on the page
			var $cancelButton = $( '<button>' )
				.addClass( 'tux-editor-cancel-button mw-ui-button mw-ui-quiet' )
				.text( mw.msg( 'tux-editor-cancel-button-label' ) )
				.on( 'click', function ( e ) {
					translateEditor.skip();
					translateEditor.hide();

					e.stopPropagation();
				} );

			var $controlButtonBlock = $( '<div>' )
				.addClass( 'twelve columns tux-editor-control-buttons' )
				.append( $requestRight, $saveButton, $skipButton, $cancelButton );

			$editorColumn.append(
				$( '<div>' )
					.addClass( 'row tux-editor-actions-block' )
					.append( $editingButtonBlock ),
				$editSummaryBlock,
				$( '<div>' )
					.addClass( 'row tux-editor-actions-block' )
					.append( $controlButtonBlock )
			);

			if ( canTranslate ) {
				var prefix = $.fn.updateTooltipAccessKeys.getAccessKeyPrefix();
				$editorColumn.append( $( '<div>' )
					.addClass( 'row shortcutinfo' )
					.text( mw.msg(
						'tux-editor-shortcut-info',
						'CTRL-ENTER',
						( prefix + 'd' ).toUpperCase(),
						'ALT',
						( prefix + 'b' ).toUpperCase()
					) )
				);
			}

			return $editorColumn;
		},

		/**
		 * Modifies the save button to provide suitable default action for *unchanged*
		 * message. It will revert back to normal save button if the text is changed.
		 *
		 * @private
		 * @param {jQuery} $button The save button.
		 */
		makeSaveButtonContextSensitive: function ( $button ) {
			var self = this;

			if ( this.message.properties.status === 'fuzzy' ) {
				$button.prop( 'disabled', false )
					.text( mw.msg( 'tux-editor-confirm-button-label' ) )
					.off( 'click' )
					.on( 'click', function ( e ) {
						self.save();
						e.stopPropagation();
					} );
			} else if ( this.message.proofreadable ) {
				$button.prop( 'disabled', false )
					.text( mw.msg( 'tux-editor-proofread-button-label' ) )
					.off( 'click' )
					.on( 'click', function ( e ) {
						$button.prop( 'disabled', true );
						self.message.proofreadAction();
						self.next();
						e.stopPropagation();
					} );
			}
		},

		/**
		 * Modifies the save button to just save the translation as usual. Whether the
		 * button is enabled or not is controlled elsewhere.
		 *
		 * @private
		 * @param {jQuery} $button The save button.
		 */
		makeSaveButtonJustSave: function ( $button ) {
			var self = this;

			$button.text( mw.msg( 'tux-editor-save-button-label' ) )
				.off( 'click' )
				.on( 'click', function ( e ) {
					self.save();
					e.stopPropagation();
				} );
		},

		/**
		 * Validate the current translation using the API
		 * and show the notices.
		 *
		 * @internal
		 */
		validateTranslation: function () {
			var translateEditor = this,
				$textarea = translateEditor.$editor.find( '.tux-textarea-translation' );

			var api = new mw.Api();

			this.validating = api.post( {
				action: 'translationcheck',
				title: this.message.title,
				translation: $textarea.val(),
				uselang: mw.config.get( 'wgUserLanguage' )
			} ).done( function ( data ) {
				var warnings = data.validation.warnings,
					errors = data.validation.errors;

				translateEditor.removeNotices( [ noticeTypes.error, noticeTypes.warning ] );

				if ( ( !warnings || !warnings.length ) &&
					( !errors || !errors.length ) ) {
					return;
				}

				// Remove useless fuzzy notice if we have more details
				translateEditor.removeNotices( noticeTypes.fuzzy );

				// Disable confirm translation button, since fuzzy translations
				// cannot be confirmed. The check for dirty state can be removed
				// to prevent translations with notices.
				if ( !translateEditor.dirty ) {
					translateEditor.$editor.find( '.tux-editor-save-button' )
						.prop( 'disabled', true );
				}

				// Don't allow users to save if there are errors but allow admins to save
				// even if there are errors.
				if ( !mw.translate.canManage() ) {
					if ( errors && errors.length > 0 ) {
						translateEditor.$editor.find( '.tux-editor-save-button' )
							.prop( 'disabled', true );
					}
				}

				translateEditor.displayNotices( warnings, noticeTypes.warning );
				translateEditor.displayNotices( errors, noticeTypes.error );

			} ).always( function () {
				translateEditor.validating = null;
			} );
		},

		/**
		 * Remove all notices of given types
		 *
		 * @internal
		 * @param {(string|string[])} types
		 */
		removeNotices: function ( types ) {
			var $tuxNotice = this.$editor.find( '.tux-notice' ),
				stringTypes = [],
				allNoticeTypes = noticeTypes.getAllClasses();

			if ( typeof types === 'string' ) {
				stringTypes.push( types );
			} else {
				stringTypes = types;
			}

			for ( var index = 0; index < stringTypes.length; index++ ) {
				if ( allNoticeTypes.indexOf( stringTypes[ index ] ) === -1 ) {
					var errMsg = 'tux: Invalid notice type removeNotice - ' + stringTypes[ index ];
					mw.log.error( errMsg );
					throw new Error( errMsg );
				}
				$tuxNotice.find( '.' + stringTypes[ index ] ).remove();
			}

			var $currentNotices = $tuxNotice.children();
			// If a single notice is shown, we can hide the more notice button,
			// and display the hidden notice.
			if ( $currentNotices.length <= 1 ) {
				this.$editor.find( '.tux-more-notices' ).addClass( 'hide' );
				$currentNotices.removeClass( 'hide' );
			}
			this.toggleMoreButtonClass();
		},

		/**
		 * Displays the supplied notice above the translation edit area.
		 * Newer notices are added to the top while older notices are
		 * added to the bottom. This also means that older notices will
		 * not be shown by default unless the user clicks "more notices" tab.
		 *
		 * @private
		 * @param {string} notice used as html for the notices display
		 * @param {string} type used to group the notices.eg: warning, diff, error
		 * @return {jQuery} the new notice element
		 */
		addNotice: function ( notice, type ) {
			var $notices = this.$editor.find( '.tux-notice' ),
				$moreNoticesTab = this.$editor.find( '.tux-more-notices' ),
				// `noticeTypes` documented above
				// eslint-disable-next-line mediawiki/class-doc
				$newNotice = $( '<div>' )
					.addClass( 'tux-notice-message ' + type )
					.html( notice );

			this.$editor.find( '.tux-notice-message' ).addClass( 'hide' );

			$notices
				.removeClass( 'hide' )
				.prepend( $newNotice );

			var noticeCount = $notices.find( '.tux-notice-message' ).length;

			if ( noticeCount > 1 ) {
				$moreNoticesTab
					.text( mw.msg( 'tux-notices-more', noticeCount - 1 ) )
					.removeClass( 'hide open' );
			} else {
				$moreNoticesTab.addClass( 'hide' );
			}
			this.toggleMoreButtonClass();

			return $newNotice;
		},

		/**
		 * Toggles the class on the more button based on the types of notice displayed, and whether
		 * the more section is expanded. This is done in order to change the background color of the
		 * button.
		 *
		 * @private
		 */
		toggleMoreButtonClass: function () {
			var $allNotices = this.$editor.find( '.tux-notice-message' ),
				errorCount = $allNotices.filter( '.tux-notice-message.' + noticeTypes.error ).length +
					$allNotices.filter( '.tux-notice-message.' + noticeTypes.translateFail ).length,
				otherErrorsCount = $allNotices.length - errorCount,
				$moreButton = this.$editor.find( '.tux-more-notices' );

			// there are other notices, and more section is expanded.
			var expanded = otherErrorsCount > 0 && $moreButton.hasClass( 'open' );
			$moreButton.toggleClass( 'tux-has-errors', errorCount > 0 && !expanded );
		},

		/**
		 * @private
		 * @return {jQuery}
		 */
		prepareInfoColumn: function () {
			var $infoColumn = $( '<div>' ).addClass( 'infocolumn' ),
				translateEditor = this;

			$infoColumn.append( $( '<div>' )
				.addClass( 'row loading' )
				.text( mw.msg( 'tux-editor-loading' ) )
			);

			if ( mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) {
				var $messageDescSaveButton = $( '<button>' )
					.addClass( 'tux-editor-savedoc-button mw-ui-button mw-ui-progressive' )
					.prop( 'disabled', true )
					.text( mw.msg( 'tux-editor-doc-editor-save' ) )
					.on( 'click', function () {
						translateEditor.saveDocumentation()
							.done( function () {
								// eslint-disable-next-line no-use-before-define
								var $descEditLink = $messageDescViewer.find( '.message-desc-edit' );
								$descEditLink.text( mw.msg( 'tux-editor-edit-desc' ) );
							} );
					} );

				var $messageDescCancelButton = $( '<button>' )
					.addClass( 'tux-editor-skipdoc-button mw-ui-button mw-ui-quiet' )
					.text( mw.msg( 'tux-editor-doc-editor-cancel' ) )
					.on( 'click', function () {
						translateEditor.hideDocumentationEditor();
					} );

				var $messageDescTextarea = $( '<textarea>' )
					.addClass( 'tux-textarea-documentation' )
					.on( 'input', function () {
						$messageDescSaveButton.prop( 'disabled', false );
					} )
					.prop( 'placeholder', mw.msg( 'tux-editor-doc-editor-placeholder' ) );

				var $messageDescEditor = $( '<div>' )
					.addClass( 'row message-desc-editor hide' )
					.append(
						$messageDescTextarea,
						$( '<div>' )
							.addClass( 'row' )
							.append(
								$messageDescSaveButton,
								$messageDescCancelButton
							)
					);

				var $messageDescViewer = $( '<div>' )
					.addClass( 'message-desc-viewer hide' )
					.append(
						$( '<div>' )
							.addClass( 'row message-desc mw-parser-output' ),
						$( '<div>' )
							.addClass( 'row message-desc-control' )
							.append( $( '<a>' )
								.attr( {
									href: mw.translate.getDocumentationEditURL(
										this.message.title.replace( /\/[a-z-]+$/, '' )
									),
									target: '_blank'
								} )
								.addClass( 'message-desc-edit' )
								.on( 'click', this.showDocumentationEditor.bind( this ) )
							)
					);

				if ( !mw.translate.canTranslate() ) {
					$messageDescViewer.find( '.message-desc-control' ).addClass( 'hide' );
				}

				$infoColumn.append(
					$messageDescEditor,
					$messageDescViewer
				);
			}

			$infoColumn.append( $( '<div>' )
				.addClass( 'row uneditable-documentation hide mw-parser-output' )
			);

			$infoColumn.append( $( '<div>' )
				.addClass( 'row edit-summaries-title hide' )
				.append(
					$( '<span>' ).text( mw.msg( 'tux-editor-latest-updates-title' ) )
				)
				.append( $( '<a>' )
					.attr(
						{
							href: mw.util.getUrl( this.message.title, { action: 'history' } ),
							target: '_blank'
						}
					)
					.text( mw.msg( 'tux-editor-all-changes' ) )
					.addClass( 'edit-summaries-all-changes' ) ) );

			$infoColumn.append( $( '<div>' )
				.addClass( 'row tm-suggestions-title hide' )
				.text( mw.msg( 'tux-editor-suggestions-title' ) )
			);

			$infoColumn.append( $( '<div>' )
				.addClass( 'row in-other-languages-title hide' )
				.text( mw.msg( 'tux-editor-in-other-languages' ) )
			);

			// The actual href is set when translationhelpers are loaded
			$infoColumn.append( $( '<div>' )
				.addClass( 'row help hide' )
				.append(
					$( '<span>' )
						.text( mw.msg( 'tux-editor-need-more-help' ) ),
					$( '<a>' )
						.attr( {
							href: '#',
							target: '_blank'
						} )
						.text( mw.msg( 'tux-editor-ask-help' ) )
				)
			);

			return $( '<div>' )
				.addClass( 'five columns infocolumn-block' )
				.append(
					$( '<span>' ).addClass( 'tux-message-editor__caret' ),
					$infoColumn
				);
		},

		/**
		 * @internal
		 * @return {boolean}
		 */
		show: function () {
			if ( !this.$editor ) {
				this.init();
			}

			var $textarea = this.$editor.find( '.editcolumn textarea' );
			// Hide all other open editors in the page
			$( '.tux-message.open' ).each( function () {
				$( this ).data( 'translateeditor' ).hide();
			} );

			this.$editor.find( '.tux-editor-save-button' ).attr( 'accesskey', 's' );
			this.$editor.find( '.tux-editor-skip-button' ).attr( 'accesskey', 'd' );
			this.$editor.find( '.tux-input-editsummary' ).attr( 'accesskey', 'b' );
			// @todo access key for the cancel button

			this.$messageItem.addClass( 'hide' );
			this.$editor.removeClass( 'hide' );
			$textarea.trigger( 'focus' );

			autosize( $textarea );
			this.resizeInsertables( $textarea );

			this.shown = true;
			this.$editTrigger.addClass( 'open' );

			// don't waste time, get ready with next message
			var $next = this.$editTrigger.next( '.tux-message' );

			if ( $next.length ) {
				$next.data( 'translateeditor' ).init();
			}

			mw.hook( 'mw.translate.editor.afterEditorShown' ).fire( this.$editor );

			return false;
		},

		/**
		 * @private
		 * @return {boolean}
		 */
		hide: function () {
			// If the user has made changes, make sure they are either
			// in process of being saved or highlighted as unsaved.
			if ( this.dirty ) {
				if ( this.saving ) {
					this.markSaving();
				} else {
					this.markUnsaved();
				}
			}

			if ( this.$editor ) {
				this.$editor.addClass( 'hide' );

				// Remove access keys to avoid duplicates in DOM (T306141)
				this.$editor.find( '.tux-editor-save-button' ).removeAttr( 'accesskey' );
				this.$editor.find( '.tux-editor-skip-button' ).removeAttr( 'accesskey' );
				this.$editor.find( '.tux-input-editsummary' ).removeAttr( 'accesskey' );
			}

			this.hideShortcuts();
			this.$editTrigger.removeClass( 'open' );
			this.$messageItem.removeClass( 'hide' );
			this.shown = false;

			return false;
		},

		/**
		 * @private
		 * @param {jQuery} toggleIcon
		 */
		infoToggle: function ( toggleIcon ) {
			this.expanded = !this.expanded;

			// Change the icon image
			toggleIcon
				.toggleClass( 'editor-expand', !this.expanded )
				.toggleClass( 'editor-contract', this.expanded )
				.attr( 'title', mw.msg( this.expanded ? 'tux-editor-collapse-tooltip' : 'tux-editor-expand-tooltip' ) );

			this.$editor.toggleClass( 'tux-message-editor--expanded', this.expanded );
		},

		/**
		 * Adds the diff between old and current definitions to the view.
		 *
		 * @internal
		 * @param {Object} definitiondiff A definitiondiff object as returned by API.
		 */
		addDefinitionDiff: function ( definitiondiff ) {
			if ( !definitiondiff || definitiondiff.error ) {
				mw.log( 'Error loading translation diff ' + definitiondiff && definitiondiff.error );
				return;
			}

			// Load the diff styles
			mw.loader.load( 'mediawiki.diff.styles' );

			var $trigger = $( '<span>' )
				.addClass( 'show-diff-link' )
				.text( mw.msg( 'tux-editor-outdated-notice-diff-link' ) )
				.on( 'click', function () {
					$( this ).parent().html( definitiondiff.html );
				} );

			this.removeNotices( noticeTypes.fuzzy );
			this.addNotice(
				mw.message( 'tux-editor-outdated-notice' ).escaped(),
				noticeTypes.diff
			).append( $trigger );
		},

		/**
		 * Attach event listeners
		 *
		 * @internal
		 */
		listen: function () {
			var translateEditor = this;

			this.$editTrigger.find( '.tux-message-item' ).on( 'click', function () {
				translateEditor.show();

				return false;
			} );
		},

		/**
		 * Makes the textarea large enough for insertables and positions the insertables.
		 *
		 * @internal
		 * @param {jQuery} $textarea Text area.
		 */
		resizeInsertables: function ( $textarea ) {
			var $buttonArea = this.$editor.find( '.tux-editor-insert-buttons' );
			var buttonAreaHeight = $buttonArea.height();
			$textarea.css( 'padding-bottom', buttonAreaHeight + 5 );
			$buttonArea.css( 'top', -buttonAreaHeight );
			autosize.update( $textarea );
		},

		/**
		 * Utility method to display a list of notices on the UI
		 *
		 * @private
		 * @param {string[]} notices
		 * @param {string} noticeType
		 */
		displayNotices: function ( notices, noticeType ) {
			for ( var index = 0; index < notices.length; ++index ) {
				this.addNotice( notices[ index ], noticeType );
			}
		},

		/**
		 * Ensures that all the notices are displayed
		 *
		 * @private
		 */
		showMoreNotices: function () {
			var $moreNoticesTab = this.$editor.find( '.tux-more-notices' );
			if ( $moreNoticesTab.hasClass( 'open' ) ) {
				return;
			}

			$moreNoticesTab.trigger( 'click' );
		},

		/**
		 * Generates the translation editor element based on target language
		 *
		 * @private
		 * @param {string} targetLangCode
		 * @return {jQuery} Returns translation editor element
		 */
		getTranslationEditor: function ( targetLangCode ) {
			var targetLangAttrib, placeholder;
			if ( targetLangCode === mw.config.get( 'wgTranslateDocumentationLanguageCode' ) ) {
				targetLangAttrib = mw.config.get( 'wgContentLanguage' );
				placeholder = mw.msg( 'tux-editor-placeholder-documentation' );
			} else {
				var userLangCode = mw.config.get( 'wgUserLanguage' );
				var targetLangName = mw.language.getData( userLangCode, 'languageNames' )[ targetLangCode ] || $.uls.data.getAutonym( targetLangCode );
				targetLangAttrib = targetLangCode;
				placeholder = mw.msg( 'tux-editor-placeholder-language', targetLangName );
			}

			var targetLangDir = $.uls.data.getDir( targetLangAttrib );

			// The following classes are used here:
			// * mw-editfont-serif
			// * mw-editfont-sans-serif
			// * mw-editfont-monospace
			return $( '<textarea>' )
				.addClass( 'tux-textarea-translation ' + this.editFontClass )
				.attr( {
					lang: targetLangAttrib,
					dir: targetLangDir
				} )
				.val( this.message.translation || '' )
				.prop( 'placeholder', placeholder );
		}
	};

	/**
	 * translateeditor PLUGIN DEFINITION
	 *
	 * @internal
	 * @param {Object} options
	 * @returns {jQuery}
	 */
	$.fn.translateeditor = function ( options ) {
		return this.each( function () {
			var $this = $( this ),
				data = $this.data( 'translateeditor' );

			if ( !data ) {
				$this.data( 'translateeditor',
					( data = new TranslateEditor( this, options ) )
				);
			}

			if ( typeof options === 'string' ) {
				data[ options ].call( $this );
			}
		} );
	};

	mw.translate.editor = mw.translate.editor || {};
	mw.translate.editor = $.extend( TranslateEditor.prototype, mw.translate.editor );

	function delayer() {
		return ( function () {
			var timer = 0;

			return function ( callback, milliseconds ) {
				clearTimeout( timer );

				if ( callback === false ) {
					// sometimes we need to just cancel the timer without
					// setting up another one
					return;
				}

				timer = setTimeout( callback, milliseconds );
			};
		}() );
	}
}() );
