( function () {
	'use strict';
	var noOfSourceUnits, noOfTranslationUnits,
		pageName = '',
		langCode = '',
		sourceUnits = [];

	/**
	 * Create translation pages using content of right hand side blocks
	 * and identifiers from left hand side blocks. Create pages only if
	 * content is not empty.
	 *
	 * @param {number} i Array index to sourceUnits.
	 * @param {string} content
	 * @return {Function} Returns a function which returns a jQuery.Promise
	 */
	function createTranslationPage( i, content ) {

		return function () {
			var api = new mw.Api();

			var identifier = sourceUnits[ i ].identifier;
			var title = 'Translations:' + pageName + '/' + identifier + '/' + langCode;
			var summary = $( '#pm-summary' ).val();

			return api.postWithToken( 'csrf', {
				action: 'edit',
				watchlist: 'nochange',
				title: title,
				text: content,
				summary: summary
			} );
		};
	}

	/**
	 * Get the old translations of a given page at given time.
	 *
	 * @internal
	 * @param {string} fuzzyTimestamp Timestamp in MediaWiki format
	 * @param {string} pageTitle
	 * @return {jQuery.Promise<Array>} Old translations
	 */
	function splitTranslationPage( fuzzyTimestamp, pageTitle ) {
		var api = new mw.Api();

		return api.get( {
			action: 'query',
			prop: 'revisions',
			rvprop: 'content',
			rvstart: fuzzyTimestamp,
			rvlimit: 1,
			formatversion: '2',
			titles: pageTitle
		} ).then( function ( data ) {
			var $errorBox = $( '.mw-tpm-sp-error__message' );
			var obj = data.query.pages[ 0 ];
			// TODO: Handle other cases such as invalid page titles ie: obj.invalid
			if ( obj === undefined || obj.missing ) {
				$errorBox.text( mw.msg( 'pm-page-does-not-exist', pageTitle ) ).removeClass( 'hide' );
				return $.Deferred().reject();
			}
			if ( obj.revisions === undefined ) {
				// the case of /en subpage where first edit is by FuzzyBot
				$errorBox.text( mw.msg( 'pm-old-translations-missing', pageTitle ) ).removeClass( 'hide' );
				return $.Deferred().reject();
			}
			return obj.revisions[ 0 ].content.split( '\n\n' );
		} );
	}

	/**
	 * Get the timestamp before FuzzyBot's first edit on page.
	 *
	 * @internal
	 * @param {string} pageTitle
	 * @return {jQuery.Promise<string>} Timestamp
	 */
	function getFuzzyTimestamp( pageTitle ) {
		var api = new mw.Api();

		// This api call returns the timestamp of FuzzyBot's edit
		return api.get( {
			action: 'query',
			prop: 'revisions',
			rvprop: 'timestamp',
			rvuser: 'FuzzyBot',
			rvdir: 'newer',
			rvlimit: 1,
			formatversion: '2',
			titles: pageTitle
		} ).then( function ( data ) {
			var $errorBox = $( '.mw-tpm-sp-error__message' );
			var obj = data.query.pages[ 0 ];

			// Page does not exist if missing field is present
			if ( obj === undefined || obj.missing === '' ) {
				$errorBox.text( mw.msg( 'pm-page-does-not-exist', pageTitle ) ).removeClass( 'hide' );
				return $.Deferred().reject();
			}

			// Page exists, but no edit by FuzzyBot
			if ( obj.revisions === undefined ) {
				$errorBox.text( mw.msg( 'pm-old-translations-missing', pageTitle ) ).removeClass( 'hide' );
				return $.Deferred().reject();
			} else {
				// FB over here refers to FuzzyBot
				var timestampFB = obj.revisions[ 0 ].timestamp;
				var dateFB = new Date( timestampFB );
				dateFB.setSeconds( dateFB.getSeconds() - 1 );
				var timestampOld = dateFB.toISOString();
				mw.log( 'New Timestamp: ' + timestampOld );
				return timestampOld;
			}
		} );
	}

	/**
	 * @typedef {Object} SourceUnit
	 * @param {string} identifier
	 * @param {string} definition
	 */

	/**
	 * Get the translation units created by Translate extension.
	 *
	 * @internal
	 * @param {string} page Page name
	 * @return {jQuery.Promise<SourceUnit[]>}
	 */
	function getSourceUnits( page ) {
		var api = new mw.Api();

		return api.get( {
			action: 'query',
			list: 'messagecollection',
			mcgroup: 'page-' + page,
			mclanguage: 'en',
			mcprop: 'definition'
		} ).then( function ( data ) {
			sourceUnits = [];
			var result = data.query.messagecollection;
			for ( var i = 0; i < result.length; i++ ) {
				var sUnit = {};
				var key = result[ i ].key;
				sUnit.identifier = key.slice( key.lastIndexOf( '/' ) + 1 );
				sUnit.definition = result[ i ].definition;
				sourceUnits.push( sUnit );
			}
			return sourceUnits;
		} ).fail( function ( code, result ) {
			// Incase the group does not exist, just return an empty array.
			var $errorContainer = $( '.mw-tpm-sp-error__message' );
			var errorMessage = mw.msg( 'pm-translation-unit-fetch-failed' );
			if (
				code === 'badparameter' &&
				result.error && result.error.info.includes( 'mcgroup' )
			) {
				errorMessage = mw.msg( 'pm-pagetitle-not-translatable', page );
			}

			$errorContainer
				.text( errorMessage )
				.removeClass( 'hide' );
			$.Deferred().reject();
		} );
	}

	/**
	 * Shift rows up by one unit. This is called after a unit is deleted.
	 *
	 * @param {jQuery} $start The starting node
	 */
	function shiftRowsUp( $start ) {
		var $current = $start,
			$next = $start.next();

		while ( $next.length ) {
			var nextVal = $next.find( '.mw-tpm-sp-unit__target' ).val();
			$current.find( '.mw-tpm-sp-unit__target' ).val( nextVal );
			$current = $next;
			$next = $current.next();
		}
		if ( $current.find( '.mw-tpm-sp-unit__source' ).val() ) {
			$current.find( '.mw-tpm-sp-unit__target' ).val( '' );
		} else {
			$current.remove();
		}
	}

	/**
	 * Shift rows down by one unit. This is called after a new empty unit is
	 * added.
	 *
	 * @param {jQuery} $nextRow The next row to start with
	 * @param {string} text The text of the next row
	 * @return {string} text The text of the last row
	 */
	function shiftRowsDown( $nextRow, text ) {
		while ( $nextRow.length ) {
			var oldText = $nextRow.find( '.mw-tpm-sp-unit__target' ).val();
			$nextRow.find( '.mw-tpm-sp-unit__target' ).val( text );
			$nextRow = $nextRow.next();
			text = oldText;
		}
		return text;
	}

	/**
	 * Create a new row of source text and target text with action icons.
	 *
	 * @param {string} sourceText
	 * @param {string} targetText
	 * @return {jQuery} newUnit The new row unit object
	 */

	function createNewUnit( sourceText, targetText ) {
		var $newUnit = $( '<div>' ).addClass( 'mw-tpm-sp-unit row' );
		var $sourceUnit = $( '<textarea>' ).addClass( 'mw-tpm-sp-unit__source five columns' )
			.prop( 'readonly', true ).attr( 'tabindex', '-1' ).val( sourceText );
		var $target = $( '<div>' ).addClass( 'five columns' );
		var $targetUnit = $( '<textarea>' ).addClass( 'mw-tpm-sp-unit__target' )
			.val( targetText ).prop( 'dir', $.uls.data.getDir( langCode ) );
		var $clearButton = $( '<button>' ).addClass( 'mw-tpm-sp-action--clear' )
			.attr( 'title', mw.msg( 'pm-clear-icon-hover-text' ) );

		$targetUnit.on( 'input', function () {
			var $input = $( this );
			if ( $input.val().length === 0 ) {
				$clearButton.addClass( 'hide' );
			} else {
				$clearButton.removeClass( 'hide' );
			}
		} ).trigger( 'input' );
		$target.append( $targetUnit, $clearButton );

		var $actionUnit = $( '<div>' ).addClass( 'mw-tpm-sp-unit__actions two columns' );
		$actionUnit.append(
			$( '<span>' ).addClass( 'mw-tpm-sp-action mw-tpm-sp-action--add' )
				.attr( 'title', mw.msg( 'pm-add-icon-hover-text' ) ),
			$( '<span>' ).addClass( 'mw-tpm-sp-action mw-tpm-sp-action--swap' )
				.attr( 'title', mw.msg( 'pm-swap-icon-hover-text' ) ),
			$( '<span>' ).addClass( 'mw-tpm-sp-action mw-tpm-sp-action--delete' )
				.attr( 'title', mw.msg( 'pm-delete-icon-hover-text' ) )
		);
		$newUnit.append( $sourceUnit, $target, $actionUnit );
		return $newUnit;
	}

	/**
	 * Display the source and target units alongwith the action icons.
	 *
	 * @param {Array} units
	 * @param {Array} translations
	 */
	function displayUnits( units, translations ) {
		noOfSourceUnits = units.length;
		noOfTranslationUnits = translations.length;
		var totalUnits = noOfSourceUnits > noOfTranslationUnits ? noOfSourceUnits : noOfTranslationUnits;
		var $unitListing = $( '.mw-tpm-sp-unit-listing' );
		$unitListing.html( '' );
		for ( var i = 0; i < totalUnits; i++ ) {
			var sourceText = '', targetText = '';
			if ( units[ i ] !== undefined ) {
				sourceText = units[ i ].definition;
			}
			if ( translations[ i ] !== undefined ) {
				targetText = translations[ i ];
			}
			var $newUnit = createNewUnit( sourceText, targetText );
			$unitListing.append( $newUnit );
		}
	}

	/**
	 * Split headers from remaining text in each translation unit if present.
	 *
	 * @internal
	 * @param {Array} translations Array of initial units obtained on splitting
	 * @return {string[]} Array having the headers split into new unit
	 */
	function splitHeaders( translations ) {
		return translations.map( function ( elem ) {
			// Check https://regex101.com/r/oT7fZ2 for details
			return elem.match( /(^==.+$|(?:(?!^==).+\n?)+)/gm );
		} ).reduce( function ( acc, val ) {
			// This should be an Array.prototype.flatMap when ES2019 is supported
			return acc.concat( val );
		}, [] );
	}

	/**
	 * Get the index of next translation unit containing h2 header.
	 *
	 * @param {number} startIndex Index to start the scan from
	 * @param {string[]} translationUnits Segmented units.
	 * @return {number} Index of the next unit found, -1 if not.
	 */
	function getHeaderUnit( startIndex, translationUnits ) {
		var regex = new RegExp( /^==[^=]+==$/m );
		for ( var i = startIndex; i < translationUnits.length; i++ ) {
			if ( regex.test( translationUnits[ i ] ) ) {
				return i;
			}
		}
		return -1;
	}

	/**
	 * Align h2 headers in the order they appear.
	 * Assumption: The source headers and translation headers appear in
	 * the same order.
	 *
	 * @internal
	 * @param {Object[]} units
	 * @param {string[]} translationUnits
	 * @return {string[]}
	 */
	function alignHeaders( units, translationUnits ) {
		// The content does not have information about the page title. Add an empty string
		// at the beginning of the translationUnits array to match the length of units and
		// translationUnits.
		if ( units.length && units[ 0 ].identifier === 'Page_display_title' ) {
			translationUnits.unshift( '' );
		}
		var tIndex = 0;
		var regex = new RegExp( /^==[^=]+==$/m );
		for ( var i = 0; i < units.length; i++ ) {
			if ( regex.test( units[ i ].definition ) ) {
				tIndex = getHeaderUnit( tIndex, translationUnits );
				var mergeText = '';
				// search is over
				if ( tIndex === -1 ) {
					break;
				}
				// remove the unit
				var matchText = translationUnits.splice( tIndex, 1 ).toString();
				var emptyCount = i - tIndex;
				if ( emptyCount > 0 ) {
					// add empty units
					while ( emptyCount !== 0 ) {
						translationUnits.splice( tIndex, 0, '' );
						emptyCount -= 1;
					}
				} else if ( emptyCount < 0 ) {
					// merge units until there is room for tIndex translation unit to
					// align with ith source unit
					while ( emptyCount !== 0 ) {
						mergeText += translationUnits.splice( i, 1 ).toString() + '\n';
						emptyCount += 1;
					}
					if ( i !== 0 ) {
						translationUnits[ i - 1 ] += '\n' + mergeText;
					} else {
						matchText = mergeText + matchText;
					}
				}
				// add the unit back
				translationUnits.splice( i, 0, matchText );
				tIndex = i + 1;
			}
		}
		return translationUnits;
	}

	/**
	 * Handler for 'Save' button click event.
	 */
	function saveHandler() {
		var list = [];

		$( '.mw-tpm-sp-error__message' ).addClass( 'hide' );
		if ( noOfSourceUnits < noOfTranslationUnits ) {
			$( '.mw-tpm-sp-error__message' ).text( mw.msg( 'pm-extra-units-warning' ) )
				.removeClass( 'hide' );
			return;
		} else {
			$( 'input' ).prop( 'disabled', true );
			$( '.mw-tpm-sp-instructions' ).addClass( 'hide' );
			for ( var i = 0; i < noOfSourceUnits; i++ ) {
				var content = $( '.mw-tpm-sp-unit__target' ).eq( i ).val();
				content = content.trim();
				if ( content !== '' ) {
					list.push( createTranslationPage( i, content ) );
				}
			}

			$.ajaxDispatcher( list, 1 ).done( function () {
				$( '#action-import' ).removeClass( 'hide' );
				$( 'input' ).prop( 'disabled', false );
				$( '.mw-tpm-sp-instructions' )
					.text( mw.msg( 'pm-on-save-message-text' ) )
					.removeClass( 'hide' );
			} ).fail( function ( errmsg ) {
				$( 'input' ).prop( 'disabled', false );
				// eslint-disable-next-line mediawiki/msg-doc
				$( '.mw-tpm-sp-error__message' ).text( mw.msg( errmsg ) ).removeClass( 'hide' );
			} );
		}
	}

	/**
	 * Handler for 'Cancel' button click event.
	 */
	function cancelHandler() {
		$( '.mw-tpm-sp-error__message' ).addClass( 'hide' );
		$( '.mw-tpm-sp-instructions' ).addClass( 'hide' );
		$( '#action-save, #action-cancel' ).addClass( 'hide' );
		$( '#action-import' ).removeClass( 'hide' );
		$( '.mw-tpm-sp-unit-listing' ).html( '' );
	}

	/**
	 * Handler for add new unit icon ('+') click event. Adds a translation unit
	 * below the current unit.
	 *
	 * @param {jQuery.Event} event
	 */
	function addHandler( event ) {
		var $nextRow = $( event.target ).closest( '.mw-tpm-sp-unit' ).next();
		var $targetUnit = $nextRow.find( '.mw-tpm-sp-unit__target' );
		var text = $targetUnit.val();
		$targetUnit.val( '' );
		$nextRow = $nextRow.next();
		text = shiftRowsDown( $nextRow, text );
		if ( text ) {
			var $newUnit = createNewUnit( '', text );
			$( '.mw-tpm-sp-unit-listing' ).append( $newUnit );
		}
		noOfTranslationUnits += 1;
	}

	/**
	 * Handler for delete icon ('-') click event. Deletes the unit and shifts
	 * the units up by one.
	 *
	 * @param {jQuery.Event} event
	 */
	function deleteHandler( event ) {
		var $rowUnit = $( event.target ).closest( '.mw-tpm-sp-unit' );
		var sourceText = $rowUnit.find( '.mw-tpm-sp-unit__source' ).val();
		if ( !sourceText ) {
			$rowUnit.remove();
		} else {
			$rowUnit.find( '.mw-tpm-sp-unit__target' ).val( '' );
			shiftRowsUp( $rowUnit );
		}
		noOfTranslationUnits -= 1;
	}

	/**
	 * Handler for clear icon click event. Clear the unit and maintains
	 * the position of other units.
	 *
	 * @param {jQuery.Event} event
	 */
	function clearContents( event ) {
		var $rowUnit = $( event.target ).closest( '.mw-tpm-sp-unit' );
		$rowUnit.find( '.mw-tpm-sp-unit__target' ).val( '' );
		$( event.target ).addClass( 'hide' );
	}

	/**
	 * Handler for swap icon click event. Swaps the text in the current unit
	 * with the text in the unit below.
	 *
	 * @param {jQuery.Event} event
	 */
	function swapHandler( event ) {
		var $rowUnit = $( event.target ).closest( '.mw-tpm-sp-unit' );
		var tempText = $rowUnit.find( '.mw-tpm-sp-unit__target' ).val();
		var nextVal = $rowUnit.next().find( '.mw-tpm-sp-unit__target' ).val();
		$rowUnit.find( '.mw-tpm-sp-unit__target' ).val( nextVal );
		$rowUnit.next().find( '.mw-tpm-sp-unit__target' ).val( tempText );
	}

	/**
	 * Handler for 'Import' button click event. Imports source and translation
	 * units and displays them.
	 *
	 * @param {jQuery.Event} e
	 */
	function importHandler( e ) {
		var $errorBox = $( '.mw-tpm-sp-error__message' ),
			$messageBox = $( '.mw-tpm-sp-instructions' );

		e.preventDefault();

		var pageTitle = $( '#title' ).val().trim();
		if ( pageTitle === '' ) {
			$errorBox.text( mw.msg( 'pm-pagetitle-missing' ) ).removeClass( 'hide' );
			return;
		}

		var titleObj = mw.Title.newFromText( pageTitle );
		$messageBox.addClass( 'hide' );
		if ( titleObj === null ) {
			$errorBox.text( mw.msg( 'pm-pagetitle-invalid' ) ).removeClass( 'hide' );
			return;
		}

		pageTitle = titleObj.getPrefixedDb();
		var slashPos = pageTitle.lastIndexOf( '/' );

		if ( slashPos === -1 ) {
			$errorBox.text( mw.msg( 'pm-langcode-missing' ) ).removeClass( 'hide' );
			return;
		}

		pageName = pageTitle.slice( 0, slashPos );
		langCode = pageTitle.slice( slashPos + 1 );

		if ( pageName === '' ) {
			$errorBox.text( mw.msg( 'pm-pagetitle-invalid' ) ).removeClass( 'hide' );
			return;
		}

		$errorBox.addClass( 'hide' );

		var fuzzyTimestamp = null;
		var units = null;
		getFuzzyTimestamp( pageTitle )
			.then( function ( response ) {
				fuzzyTimestamp = response;
				return getSourceUnits( pageName );
			} )
			.then( function ( response ) {
				units = response;
				return splitTranslationPage( fuzzyTimestamp, pageTitle );
			} )
			.then( function ( translations ) {
				noOfSourceUnits = units.length;
				var translationUnits = splitHeaders( translations );
				translationUnits = alignHeaders( units, translationUnits );
				noOfTranslationUnits = translationUnits.length;
				displayUnits( units, translationUnits );
				$( '#action-save, #action-cancel' ).removeClass( 'hide' );
				$( '#action-import' ).addClass( 'hide' );
				$messageBox.text( mw.msg( 'pm-on-import-message-text' ) ).removeClass( 'hide' );
			} );
	}

	/**
	 * Listens to various click events
	 */
	function listen() {
		var $listing = $( '.mw-tpm-sp-unit-listing' );

		$( '#mw-tpm-sp-primary-form' ).on( 'submit', importHandler );
		$( '#action-import' ).on( 'click', importHandler );
		$( '#action-save' ).on( 'click', saveHandler );
		$( '#action-cancel' ).on( 'click', cancelHandler );
		$listing.on( 'click', '.mw-tpm-sp-action--swap', swapHandler );
		$listing.on( 'click', '.mw-tpm-sp-action--delete', deleteHandler );
		$listing.on( 'click', '.mw-tpm-sp-action--add', addHandler );
		$listing.on( 'click', '.mw-tpm-sp-action--clear', clearContents );
	}

	$( listen );

	mw.translate = mw.translate || {};
	mw.translate = $.extend( mw.translate, {
		getSourceUnits: getSourceUnits,
		getFuzzyTimestamp: getFuzzyTimestamp,
		splitTranslationPage: splitTranslationPage,
		splitHeaders: splitHeaders,
		alignHeaders: alignHeaders
	} );

}() );
