/*!
 * VisualEditor UserInterface WindowAction class.
 *
 * @copyright See AUTHORS.txt
 */

/**
 * Window action.
 *
 * @class
 * @extends ve.ui.Action
 * @constructor
 * @param {ve.ui.Surface} surface Surface to act on
 * @param {string} [source]
 */
ve.ui.WindowAction = function VeUiWindowAction() {
	// Parent constructor
	ve.ui.WindowAction.super.apply( this, arguments );
};

/* Inheritance */

OO.inheritClass( ve.ui.WindowAction, ve.ui.Action );

/* Static Properties */

ve.ui.WindowAction.static.name = 'window';

ve.ui.WindowAction.static.methods = [ 'open', 'close', 'toggle' ];

/* Methods */

/**
 * Open a window.
 *
 * @param {string} name Symbolic name of window to open
 * @param {Object} [data] Window opening data
 * @param {string} [action] Action to execute after opening, or immediately if the window is already open
 * @return {boolean|jQuery.Promise} Action was executed; if a Promise, it'll resolve once the action is finished executing
 */
ve.ui.WindowAction.prototype.open = function ( name, data = {}, action = null ) {
	if ( !data ) {
		data = {};
		OO.ui.warnDeprecation( 'WindowAction#open data argument must be undefined or object' );
	}
	const windowType = this.getWindowType( name ),
		windowManager = this.getWindowManager( windowType ),
		currentWindow = windowManager.getCurrentWindow(),
		autoClosePromises = [],
		surface = this.surface,
		surfaceFragment = data.fragment || surface.getModel().getFragment( undefined, true ),
		dir = surface.getView().getSelectionDirectionality(),
		windowClass = ve.ui.windowFactory.lookup( name ),
		isFragmentWindow = !!windowClass.prototype.getFragment,
		mayRequireFragment = isFragmentWindow ||
			// HACK: Pass fragment to toolbar dialogs as well
			windowType.name === 'toolbar',
		// TODO: Add 'doesHandleSource' method to factory
		sourceMode = surface.getMode() === 'source' && !windowClass.static.handlesSource,
		openDeferred = ve.createDeferred(),
		openPromise = openDeferred.promise();

	ve.track(
		'activity.' + name,
		{ action: 'window-open-from-' + ( this.source || 'command' ) }
	);

	if ( !windowManager ) {
		return false;
	}

	let fragmentPromise;
	let originalFragment;
	if ( !mayRequireFragment ) {
		fragmentPromise = ve.createDeferred().resolve().promise();
	} else if ( sourceMode ) {
		const text = surfaceFragment.getText( true );
		originalFragment = surfaceFragment;

		fragmentPromise = surfaceFragment.convertFromSource( text ).then( ( selectionDocument ) => {
			const tempSurfaceModel = new ve.dm.Surface( selectionDocument ),
				tempFragment = tempSurfaceModel.getLinearFragment(
					// TODO: Select all content using content offset methods
					new ve.Range(
						1,
						Math.max( 1, selectionDocument.getDocumentRange().end - 1 )
					)
				);

			return tempFragment;
		} );
	} else {
		fragmentPromise = ve.createDeferred().resolve( surfaceFragment ).promise();
	}

	data = ve.extendObject( { dir }, data, { surface, $returnFocusTo: null } );

	if ( windowType.name === 'toolbar' || windowType.name === 'inspector' ) {
		// Auto-close the current window if it is different to the one we are
		// trying to open.
		// TODO: Make auto-close a window manager setting
		if ( currentWindow && currentWindow.constructor.static.name !== name ) {
			autoClosePromises.push( windowManager.closeWindow( currentWindow ).closed );
		}
	}

	// If we're opening a dialog, close all inspectors first
	if ( windowType.name === 'dialog' ) {
		const inspectorWindowManager = this.getWindowManager( { name: 'inspector' } );
		const currentInspector = inspectorWindowManager.getCurrentWindow();
		if ( currentInspector ) {
			autoClosePromises.push( inspectorWindowManager.closeWindow( currentInspector ).closed );
		}
	}

	fragmentPromise.then( ( fragment ) => {
		ve.extendObject( data, { fragment } );

		ve.promiseAll( autoClosePromises ).always( () => {
			windowManager.getWindow( name ).then( ( win ) => {
				if ( windowManager.getCurrentWindow() === win ) {
					// If the window was already open we are just passing a command
					// to it, e.g. 'findNext' in FindAndReplaceDialog, so there is
					// no need to touch the selection.
					if ( action ) {
						win.executeAction( action );
					}
					return;
				}

				const instance = windowManager.openWindow( win, data );

				if ( sourceMode ) {
					win.sourceMode = sourceMode;
				}

				if ( !win.constructor.static.activeSurface ) {
					surface.getView().deactivate( false );
				} else {
					instance.opened.then( () => {
						// The dialog still automatically receives focus after opening, move it back to the surface.
						const previousSelection = surface.getModel().getSelection();
						// On deactivated surfaces (e.g. those using nullSelectionOnBlur), the native selection is
						// removed after a setTimeout to fix a bug in iOS (T293661, in ve.ce.Surface#deactivate).
						// Ensure that we restore the selection **after** this happens, otherwise the surface will
						// get re-blurred. (T318720)
						setTimeout( () => {
							surface.getView().focus();
							if ( !previousSelection.isNull() ) {
								surface.getModel().setSelection( previousSelection );
							}
						} );
					} );
				}

				instance.opened.then( () => {
					if ( sourceMode ) {
						// HACK: initialFragment/previousSelection is assumed to be in the visible surface
						win.initialFragment = null;
						win.previousSelection = null;
					}
				} );
				instance.opened.always( () => {
					// This uses .always() so that the action is executed even if the window is already open
					// (in which case opening it again fails). Hopefully we'll never have a situation where
					// it's closed, the opening fails for some reason, and then weird things happen.
					if ( action ) {
						win.executeAction( action );
					}
					openDeferred.resolve( instance );
				} );

				if ( !win.constructor.static.activeSurface ) {
					windowManager.once( 'closing', () => {
						// Collapsed mobile selection: We need to re-activate the surface in case an insertion
						// annotation was generated. We also need to do it during the same event cycle otherwise
						// the device may not open the virtual keyboard, so use the 'closing' event. (T203517)
						if ( OO.ui.isMobile() && surface.getModel().getSelection().isCollapsed() ) {
							surface.getView().activate();
						} else {
							// Otherwise use the `closed` promise to wait until the dialog has performed its actions,
							// such as creating new annotations or moving focus, before re-activating.
							instance.closed.then( () => {
								// Don't activate if mobile and expanded
								if ( !( OO.ui.isMobile() && !surface.getModel().getSelection().isCollapsed() ) ) {
									surface.getView().activate();
								}
							} );
						}
					} );
				}

				instance.closed.then( ( closedData ) => {
					// Sequence-triggered window closed without action, undo
					if ( data.strippedSequence && !( closedData && closedData.action ) ) {
						surface.getModel().undo();
						// Prevent redoing (which would remove the typed text)
						surface.getModel().truncateUndoStack();
						surface.getModel().emit( 'history' );
					}
					if ( sourceMode && fragment && fragment.getSurface().hasBeenModified() ) {
						// Action may be async, so we use auto select to ensure the content is selected
						originalFragment.setAutoSelect( true );
						originalFragment.insertDocument( fragment.getDocument() );
					}
					surface.getView().emit( 'position' );
				} );
			} );
		} );
	} );

	return openPromise;
};

/**
 * Close a window
 *
 * @param {string} name Symbolic name of window to open
 * @param {Object} [data] Window closing data
 * @return {boolean|jQuery.Promise} Action was executed; if a Promise, it'll resolve once the action is finished executing
 */
ve.ui.WindowAction.prototype.close = function ( name, data ) {
	const windowType = this.getWindowType( name ),
		windowManager = this.getWindowManager( windowType );

	if ( !windowManager ) {
		return false;
	}

	return windowManager.closeWindow( name, data );
};

/**
 * Toggle a window between open and close
 *
 * @param {string} name Symbolic name of window to open or close
 * @param {Object} [data] Window opening or closing data
 * @return {boolean|jQuery.Promise} Action was executed; if a Promise, it'll resolve once the action is finished executing
 */
ve.ui.WindowAction.prototype.toggle = function ( name, data ) {
	const windowType = this.getWindowType( name ),
		windowManager = this.getWindowManager( windowType );

	if ( !windowManager ) {
		return false;
	}

	const win = windowManager.getCurrentWindow();
	if ( !win || win.constructor.static.name !== name ) {
		return this.open( name, data );
	} else {
		return this.close( name, data );
	}
};

/**
 * @typedef {Object} ve.ui.WindowAction.WindowType
 * @property {string|null} name Window name ('inspector', 'toolbar', 'sidebar', dialog' or null)
 * @property {string} [position] Window position (for toolbar dialogs)
 */

/**
 * Get the specified window type
 *
 * @param {string} name Window name
 * @return {ve.ui.WindowAction.WindowType}
 */
ve.ui.WindowAction.prototype.getWindowType = function ( name ) {
	const windowClass = ve.ui.windowFactory.lookup( name );
	if ( !windowClass ) {
		throw new Error( 'No window class registered with the name "' + name + '"' );
	}
	if ( windowClass.prototype instanceof ve.ui.FragmentInspector ) {
		return { name: 'inspector' };
	} else if ( windowClass.prototype instanceof ve.ui.ToolbarDialog ) {
		return {
			name: 'toolbar',
			position: windowClass.static.position
		};
	} else if ( windowClass.prototype instanceof ve.ui.SidebarDialog ) {
		return { name: 'sidebar' };
	} else if ( windowClass.prototype instanceof OO.ui.Dialog ) {
		return { name: 'dialog' };
	}
	return { name: null };
};

/**
 * Get the window manager for a specified window type
 *
 * @param {ve.ui.WindowAction.WindowType} windowType Window type object. See #getWindowType
 * @return {ve.ui.WindowManager|null} Window manager
 */
ve.ui.WindowAction.prototype.getWindowManager = function ( windowType ) {
	switch ( windowType.name ) {
		case 'inspector':
			return this.surface.getContext().getInspectors();
		case 'toolbar':
			return this.surface.getToolbarDialogs( windowType.position );
		case 'sidebar':
			return this.surface.getSidebarDialogs();
		case 'dialog':
			return this.surface.getDialogs();
	}
	return null;
};

/* Registration */

ve.ui.actionFactory.register( ve.ui.WindowAction );
