const ResizingDragBar = require( './ResizingDragBar.js' );
const TwoPaneLayout = require( './TwoPaneLayout.js' );
const ErrorLayout = require( './ErrorLayout.js' );
const ManualWidget = require( './ManualWidget.js' );
const localStorage = require( 'mediawiki.storage' ).local;

/**
 * @class
 * @param {Object} context The WikiEditor context.
 */
function RealtimePreview( context ) {
	this.context = context;
	this.configData = mw.loader.moduleRegistry[ 'ext.wikiEditor' ].script.files[ 'data.json' ];
	// Preference name, must match what's in extension.json and Hooks.php.
	this.prefName = 'wikieditor-realtimepreview';
	this.userPref = this.getUserPref();
	this.enabled = this.userPref;
	this.twoPaneLayout = new TwoPaneLayout();
	this.pagePreview = require( 'mediawiki.page.preview' );
	// @todo This shouldn't be required, but the preview element is added in PHP
	// and can have attributes with values (such as `dir`) that aren't easily accessible from here,
	// and we need to duplicate here what Live Preview does in core.
	const $previewContent = $( '#wikiPreview .mw-content-ltr, #wikiPreview .mw-content-rtl' ).first().clone();
	this.$previewNode = $( '<div>' )
		.addClass( 'ext-WikiEditor-realtimepreview-preview' )
		.attr( 'tabindex', '1' ) // T317108
		.append( $previewContent );
	this.createToolbarButton();

	// Loading bar.
	this.$loadingBar = $( '<div>' ).addClass( 'ext-WikiEditor-realtimepreview-loadingbar' ).append( '<div>' );
	this.$loadingBar.hide();

	// Error layout.
	this.errorLayout = new ErrorLayout();
	this.errorLayout.getReloadButton().connect( this, {
		click: function () {
			this.doRealtimePreview( true );
			mw.hook( 'ext.WikiEditor.realtimepreview.reloadError' ).fire( this );
		}.bind( this )
	} );

	// Manual reload button.
	this.reloadButton = new OO.ui.ButtonWidget( {
		// Add the .tool class.
		classes: [ 'tool', 'ext-WikiEditor-reloadButton' ],
		icon: 'reload',
		framed: false,
		accessKey: mw.msg( 'accesskey-wikieditor-realtimepreview' ),
		title: mw.msg( 'wikieditor-realtimepreview-reload-title' )
	} );
	this.reloadButton.connect( this, {
		click: function () {
			// Only refresh the preview if we're enabled.
			if ( this.enabled ) {
				this.doRealtimePreview( true );
			}
			// Let other things happen after reloading.
			// The button used to appear only when hovering on the preview pane, hence the hook name.
			mw.hook( 'ext.WikiEditor.realtimepreview.reloadHover' ).fire( this );
		}.bind( this )
	} );
	this.reloadButton.toggle( this.enabled );

	// Manual mode widget.
	this.manualWidget = new ManualWidget( this );
	// Set up a property for reducedMotion — useful for customising the UI message.
	this.reducedMotion = window.matchMedia( '(prefers-reduced-motion: reduce)' ).matches;
	// If the user has "prefers-reduced-motion" set, force us into manual mode.
	this.inManualMode = this.reducedMotion;

	this.twoPaneLayout.getPane2().append( this.manualWidget.$element, this.$loadingBar, this.$previewNode, this.errorLayout.$element );
	this.eventNames = 'change.realtimepreview input.realtimepreview cut.realtimepreview paste.realtimepreview';
	// Used to ensure we wait for a response before making new requests.
	this.isPreviewing = false;
	this.previewPending = false;
	this.lastWikitext = null;
	// Used to average response times and automatically disable realtime preview if it's very slow.
	this.responseTimes = [];
	// Used to hide the 'abort' error when submitting the #editform.
	this.isSubmitting = false;
}

/**
 * @private
 */
RealtimePreview.prototype.createToolbarButton = function () {
	const $uiText = this.context.$ui.find( '.wikiEditor-ui-text' );

	// Fix the height of the textarea, before adding a resizing bar below it.
	const height = this.context.$textarea.height();
	$uiText.css( 'height', height + 'px' );
	this.context.$textarea.removeAttr( 'rows cols' );
	this.context.$textarea.addClass( 'ext-WikiEditor-realtimepreview-textbox' );

	// Add the resizing bar.
	const bottomDragBar = new ResizingDragBar( { isEW: false, id: 'ext-WikiEditor-bottom-dragbar' } );
	$uiText.after( bottomDragBar.$element );

	// Create and configure the toolbar button.
	this.button = new OO.ui.ToggleButtonWidget( {
		label: mw.msg( 'wikieditor-realtimepreview-preview' ),
		icon: 'article',
		value: this.enabled,
		framed: false,
		// T305953; So we can find usage of this class later: .tool
		classes: [ 'tool', 'ext-WikiEditor-realtimepreview-button' ]
	} );
	this.button.connect( this, { change: [ this.toggle, true ] } );
	if ( !this.isScreenWideEnough() ) {
		this.enabled = false;
		this.button.toggle( false );
	}

	// Hide or show the preview and toolbar button when the window is resized.
	$( window ).on( 'resize', this.enableFeatureWhenScreenIsWideEnough.bind( this ) );

	// Remove the old onboarding-status storage that was discontinued in March 2023.
	localStorage.remove( 'WikiEditor-RealtimePreview-onboarding-dismissed' );
};

/**
 * @public
 * @return {jQuery}
 */
RealtimePreview.prototype.getToolbarButton = function () {
	return this.button.$element;
};

/**
 * @public
 * @return {jQuery}
 */
RealtimePreview.prototype.getToolbarReloadButton = function () {
	return this.reloadButton.$element;
};

/**
 * Get the user preference for Realtime Preview.
 *
 * @public
 * @return {boolean}
 */
RealtimePreview.prototype.getUserPref = function () {
	return ( typeof this.userPref !== 'undefined' ) ? this.userPref : mw.user.options.get( this.prefName ) > 0;
};

/**
 * Enable or disable Realtime Preview.
 *
 * @param {boolean} [enable=true] Whether to enable or disable.
 * @param {boolean} [saveUserPref=true] Whether to save the user preference.
 * @public
 */
RealtimePreview.prototype.setEnabled = function ( enable, saveUserPref ) {
	// Set this.enabled to the opposite of what we want, and then toggle it to the desired state.
	this.enabled = ( typeof enable === 'boolean' ) ? !enable : false;
	this.toggle( saveUserPref );
};

/**
 * Save the user preference for Realtime Preview.
 *
 * @private
 */
RealtimePreview.prototype.saveUserPref = function () {
	this.userPref = this.enabled ? 1 : 0;
	( new mw.Api() ).saveOption( this.prefName, this.userPref );
};

/**
 * Toggle the two-pane preview display.
 *
 * @param {boolean} [saveUserPref=true] Whether to save the user preference.
 * @private
 */
RealtimePreview.prototype.toggle = function ( saveUserPref ) {
	const $uiText = this.context.$ui.find( '.wikiEditor-ui-text' );
	const $textarea = this.context.$textarea;
	const $form = $textarea.parents( 'form' );

	// Save the current cursor selection and focused element.
	const cursorPos = $textarea.textSelection( 'getCaretPosition', { startAndEnd: true } );
	const focusedElement = document.activeElement;

	// Remove or add the layout to the DOM.
	if ( this.enabled ) {
		// Move height from the TwoPaneLayout to the text UI div.
		$uiText.css( 'height', this.twoPaneLayout.$element.height() + 'px' );

		// Put the text div back to being after the layout, and then hide the layout.
		this.twoPaneLayout.$element.after( $uiText );
		this.twoPaneLayout.$element.hide();

		// Remove the keyup handler.
		$textarea.off( this.eventNames );
		$form.off( 'reset.realtimepreview' );

		// Remove the submit handler.
		$form.off( 'submit.realtimepreview' );
		this.isSubmitting = false;

		// Let other things happen after disabling.
		mw.hook( 'ext.WikiEditor.realtimepreview.disable' ).fire( this );

		// Remove the reload button from the toolbar.
		this.reloadButton.toggle( false );

	} else {
		// Add the layout before the text div of the UI and then move the text div into it.
		$uiText.before( this.twoPaneLayout.$element );
		this.twoPaneLayout.setPane1( $uiText );
		this.twoPaneLayout.$element.show();

		// Move explicit height from text-ui (which may have been set via manual resizing), to panes.
		this.twoPaneLayout.$element.css( 'height', $uiText.height() + 'px' );
		$uiText.css( 'height', '100%' );

		// Load the preview when enabling,
		this.doRealtimePreview();
		// and also on keyup, change, paste etc.
		$textarea
			.off( this.eventNames )
			.on( this.eventNames, this.getEventHandler() );
		$form.off( 'reset.realtimepreview' )
			.on( 'reset.realtimepreview', this.getEventHandler() );

		// Hide or show the manual-reload message bar.
		this.manualWidget.toggle( this.inManualMode );

		$form.on( 'submit.realtimepreview', () => {
			this.isSubmitting = true;
		} );

		// Show the reload button in the toolbar.
		this.reloadButton.toggle( true );

		// Let other things happen after enabling.
		mw.hook( 'ext.WikiEditor.realtimepreview.enable' ).fire( this );
	}

	// Restore current selection.
	$textarea.textSelection( 'setSelection', { start: cursorPos[ 0 ], end: cursorPos[ 1 ] } );
	$textarea.textSelection( 'scrollToCaretPosition' );
	// Focus on whatever had focus before, in case it wasn't the textarea.
	focusedElement.focus();

	// Record the toggle state and update the button.
	this.enabled = !this.enabled;
	this.button.$element.toggleClass( 'tool-active', this.enabled ); // T305953
	this.button.setFlags( { progressive: this.enabled } );

	if ( typeof saveUserPref === 'undefined' || ( typeof saveUserPref === 'boolean' && saveUserPref ) ) {
		this.saveUserPref();
	}
};

/**
 * @public
 * @return {Function}
 */
RealtimePreview.prototype.getEventHandler = function () {
	return mw.util.debounce(
		() => {
			// Only do preview if we're not in manual mode (as set in this.checkResponseTimes()).
			if ( !this.inManualMode ) {
				this.doRealtimePreview();
			}
		},
		this.configData.realtimeDebounce
	);
};

/**
 * Check if screen meets minimum width requirement for Realtime Preview.
 *
 * @public
 * @return {boolean}
 */
RealtimePreview.prototype.isScreenWideEnough = function () {
	return this.context.$ui.width() > 600;
};

/**
 * Display feature (buttons and functionality) only when screen is wide enough
 *
 * @private
 */
RealtimePreview.prototype.enableFeatureWhenScreenIsWideEnough = function () {
	const previewButtonIsVisible = this.button.isVisible();
	const isScreenWideEnough = this.isScreenWideEnough();
	if ( !isScreenWideEnough && previewButtonIsVisible ) {
		this.button.toggle( false );
		this.reloadButton.setDisabled( true );
		if ( this.enabled ) {
			this.setEnabled( false, false );
		}
	} else if ( isScreenWideEnough && !previewButtonIsVisible ) {
		this.button.toggle( true );
		this.reloadButton.setDisabled( false );
		// if user preference and realtime disable
		if ( !this.enabled && this.getUserPref() ) {
			this.setEnabled( true, false );
		}
	}
};

/**
 * @private
 * @param {jQuery} $msg
 */
RealtimePreview.prototype.showError = function ( $msg ) {
	if ( this.isSubmitting ) {
		// Do not show any error if the form has been submitted (the page is being reloaded and
		// any preview request will have been aborted).
		return;
	}
	this.$previewNode.hide();
	this.manualWidget.toggle( false );
	// There is no need for a default message because mw.Api.getErrorMessage() will
	// always provide something (even for no network connection, server-side fatal errors, etc.).
	this.errorLayout.setMessage( $msg );
	this.errorLayout.toggle( true );
};

/**
 * @private
 * @param {number} time
 */
RealtimePreview.prototype.checkResponseTimes = function ( time ) {
	// Don't track response times if we're already in manual mode or an error is shown.
	if ( this.inManualMode || this.errorLayout.isVisible() ) {
		return;
	}

	this.responseTimes.push( Date.now() - time );
	if ( this.responseTimes.length < 3 ) {
		return;
	}

	const totalResponseTime = this.responseTimes.reduce( ( a, b ) => a + b, 0 );

	if ( ( totalResponseTime / this.responseTimes.length ) > this.configData.realtimeDisableDuration ) {
		this.inManualMode = true;
		this.manualWidget.toggle( true );
		mw.hook( 'ext.WikiEditor.realtimepreview.stop' ).fire( this );
	}

	this.responseTimes.shift();
};

/**
 * @private
 * @param {boolean} forceUpdate For the preview to update, even if the wikitext is unchanged,
 *  e.g. when the user presses the 'reload' button.
 */
RealtimePreview.prototype.doRealtimePreview = function ( forceUpdate ) {
	// Wait for a response before making any new requests.
	if ( this.isPreviewing ) {
		// Queue up one final preview once this one finishes.
		this.previewPending = true;
		return;
	}

	const $textareaNode = this.context.$textarea;
	const wikitext = $textareaNode.textSelection( 'getContents' );
	if ( !forceUpdate && wikitext === this.lastWikitext ) {
		// Wikitext unchanged, no update necessary
		return;
	}
	this.lastWikitext = wikitext;

	this.isPreviewing = true;
	this.$loadingBar.show();
	this.$previewNode.show();
	this.reloadButton.setDisabled( true );
	this.manualWidget.setDisabled( true );
	this.errorLayout.toggle( false );
	const loadingSelectors = this.pagePreview.getLoadingSelectors()
		// config.$previewNode below is a clone of #wikiPreview with a different selector!
		// config.$diffNode defaults to #wikiDiff but is disabled below and never updated.
		.filter( ( selector ) => selector.indexOf( '#wiki' ) !== 0 );
	loadingSelectors.push( '.ext-WikiEditor-realtimepreview-preview' );
	loadingSelectors.push( '.ext-WikiEditor-ManualWidget' );
	loadingSelectors.push( '.ext-WikiEditor-realtimepreview-ErrorLayout' );
	const time = Date.now();

	this.pagePreview.doPreview( {
		$textareaNode: $textareaNode,
		$previewNode: this.$previewNode,
		$spinnerNode: false,
		loadingSelectors: loadingSelectors,
		// Don't hide the diff view, if visible.
		$diffNode: null
	} ).then(
		() => {
			this.errorLayout.toggle( false );
		},
		( code, result ) => {
			this.showError( ( new mw.Api() ).getErrorMessage( result ) );
			mw.log.error( 'WikiEditor realtime preview error', result );
		}
	).then( () => {
		this.$loadingBar.hide();
		this.reloadButton.setDisabled( false );
		// Show the manual mode if applicable (but not if an error is displayed).
		this.manualWidget.toggle( this.inManualMode && !this.errorLayout.isVisible() );
		this.manualWidget.setDisabled( false );
		this.isPreviewing = false;
		this.checkResponseTimes( time );

		if ( this.previewPending ) {
			this.previewPending = false;
			this.doRealtimePreview();
		}
		mw.hook( 'ext.WikiEditor.realtimepreview.loaded' ).fire( this );
	} );
};

module.exports = RealtimePreview;
