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

/**
 * A surface is a top-level object which contains both a surface model and a surface view.
 *
 * @class
 * @abstract
 * @extends OO.ui.Widget
 *
 * @constructor
 * @param {ve.init.Target} target Target the surface belongs to
 * @param {HTMLDocument|Array|ve.dm.ElementLinearData|ve.dm.Document|ve.dm.Surface} dataOrDocOrSurface Document data, document model, or surface model to edit
 * @param {Object} [config] Configuration options
 * @param {ve.dm.BranchNode} [config.attachedRoot] Node to surface, if ve.dm.Document passed in
 * @param {string} config.mode Editing mode, either "visual" or "source"
 * @param {jQuery} [config.$scrollContainer] The scroll container of the surface
 * @param {jQuery} [config.$scrollListener] The scroll listener of the surface
 * @param {jQuery} [config.$overlayContainer] Clipping container for local overlays, defaults to surface view
 * @param {number} [config.overlayPadding] Padding beween local overlays and clipping container
 * @param {ve.ui.CommandRegistry} [config.commandRegistry] Command registry to use
 * @param {ve.ui.SequenceRegistry} [config.sequenceRegistry] Sequence registry to use
 * @param {ve.ui.DataTransferHandlerFactory} [config.dataTransferHandlerFactory] Data transfer handler factory to use
 * @param {string[]|null} [config.includeCommands] List of commands to include, null for all registered commands
 * @param {string[]} [config.excludeCommands] List of commands to exclude
 * @param {Object} [config.importRules={}] Import rules
 * @param {boolean} [config.multiline=true] Multi-line surface
 * @param {string} [config.placeholder] Placeholder text to display when the surface is empty
 * @param {string} [config.readOnly=false] Surface is read-only
 * @param {string} [config.nullSelectionOnBlur=true] Surface selection is set to null on blur
 * @param {string} [config.inDialog=''] The name of the dialog this surface is in
 * @param {boolean} [config.inTargetWidget=false] The surface is in a target widget
 */
ve.ui.Surface = function VeUiSurface( target, dataOrDocOrSurface, config ) {
	config = config || {};

	// Parent constructor
	ve.ui.Surface.super.call( this, config );

	// Properties
	this.target = target;
	this.$scrollContainer = config.$scrollContainer || $( this.getClosestScrollableElementContainer() );
	this.$scrollListener = config.$scrollListener || $( this.getElementWindow() );
	this.inDialog = config.inDialog || '';
	this.inTargetWidget = !!config.inTargetWidget;
	this.mode = config.mode;

	// The following classes are used here:
	// * ve-ui-overlay-global-mobile
	// * ve-ui-overlay-global-desktop
	this.globalOverlay = new ve.ui.Overlay( { classes: [ 've-ui-overlay-global', 've-ui-overlay-global-' + ( OO.ui.isMobile() ? 'mobile' : 'desktop' ) ] } );
	this.localOverlay = new ve.ui.Overlay( { classes: [ 've-ui-overlay-local' ] } );
	this.$selections = $( '<div>' ).addClass( 've-ui-surface-selections' );
	this.$blockers = $( '<div>' );
	this.$controls = $( '<div>' );
	this.$menus = $( '<div>' );
	this.$placeholder = $( '<div>' ).addClass( 've-ui-surface-placeholder' );
	this.commandRegistry = config.commandRegistry || ve.ui.commandRegistry;
	this.sequenceRegistry = config.sequenceRegistry || ve.ui.sequenceRegistry;
	this.dataTransferHandlerFactory = config.dataTransferHandlerFactory || ve.ui.dataTransferHandlerFactory;
	this.commands = OO.simpleArrayDifference(
		config.includeCommands || this.commandRegistry.getNames(), config.excludeCommands || []
	);
	this.triggerListener = new ve.TriggerListener( this.commands, this.commandRegistry );
	if ( dataOrDocOrSurface instanceof ve.dm.Surface ) {
		this.model = dataOrDocOrSurface;
	} else {
		let documentModel;
		if ( dataOrDocOrSurface instanceof ve.dm.Document ) {
			// ve.dm.Document
			documentModel = dataOrDocOrSurface;
		} else if ( dataOrDocOrSurface instanceof ve.dm.ElementLinearData || Array.isArray( dataOrDocOrSurface ) ) {
			// LinearData or raw linear data
			documentModel = new ve.dm.Document( dataOrDocOrSurface );
		} else {
			// HTMLDocument
			documentModel = ve.dm.converter.getModelFromDom( dataOrDocOrSurface );
		}
		this.model = this.createModel( documentModel, config.attachedRoot );
	}
	this.view = this.createView( this.model );
	this.dialogs = this.createDialogWindowManager();
	this.importRules = config.importRules || {};
	this.multiline = config.multiline !== false;
	this.context = this.createContext( {
		$popupContainer: config.$overlayContainer,
		popupPadding: config.overlayPadding
	} );
	this.progresses = [];
	this.showProgressDebounced = ve.debounce( this.showProgress.bind( this ) );
	this.scrollSelectionIntoViewDebounced = ve.debounce( this.scrollSelectionIntoView.bind( this ), 500 );
	this.debugBar = null;
	this.placeholder = null;
	this.placeholderVisible = false;
	this.setPlaceholder( config.placeholder );
	this.setReadOnly( !!config.readOnly );
	this.nullSelectionOnBlur = config.nullSelectionOnBlur !== false;
	this.completion = new ve.ui.CompletionWidget( this );

	// Deprecated, use this.padding.top
	this.toolbarHeight = 0;
	this.padding = {
		top: 0,
		right: 0,
		bottom: 0,
		left: 0
	};
	// Intiailised on first use
	this.toolbarDialogs = {};

	// Events
	this.getModel().connect( this, {
		select: 'onModelSelect',
		blur: 'onModelBlur',
		focus: 'onModelFocus'
	} );
	this.getModel().getDocument().connect( this, { transact: 'onDocumentTransact' } );
	this.getView().connect( this, {
		position: 'onViewPosition',
		activation: 'onViewActivation'
	} );
	this.getContext().connect( this, { resize: ve.debounce( this.onContextResize.bind( this ) ) } );

	// Initialization
	if ( OO.ui.isMobile() ) {
		// Mobile context is almost fullscreen and must appear on top of other elements (T308716)
		this.globalOverlay.$element.append( this.context.$element );
	} else {
		this.$menus.append( this.context.$element );
	}
	this.$menus.append( this.completion.$element );
	this.$element
		// The following classes are used here:
		// * ve-ui-surface-visual
		// * ve-ui-surface-source
		.addClass( 've-ui-surface ve-ui-surface-' + this.mode )
		.append( this.view.$element );
	if ( this.mode === 'source' ) {
		// Separate class to make it easier to override
		this.getView().$element.add( this.$placeholder )
			.addClass( 've-ui-surface-source-font' );
	}
	this.view.$element.after( this.localOverlay.$element );
	this.localOverlay.$element.append( this.$selections, this.$blockers, this.$controls, this.$menus );
	this.globalOverlay.$element.append( this.dialogs.$element );
};

/* Inheritance */

OO.inheritClass( ve.ui.Surface, OO.ui.Widget );

/* Events */

/**
 * When a surface has been initialized
 *
 * @event ve.ui.Surface#ready
 */

/**
 * When a surface is destroyed.
 *
 * @event ve.ui.Surface#destroy
 */

/**
 * The surface was scrolled programmatically
 * as a result of a native selection change
 *
 * @event ve.ui.Surface#scroll
 */

/**
 * The surface has been submitted by user action, e.g. Ctrl+Enter
 *
 * @event ve.ui.Surface#submit
 */

/**
 * The surface has been cancelled by user action, e.g. Escape
 *
 * @event ve.ui.Surface#cancel
 */

/**
 * The surface read-only state has changed
 *
 * @event ve.ui.Surface#readOnly
 * @param {boolean} readOnly The surface is read-only
 */

/* Methods */

/**
 * Destroy the surface, releasing all memory and removing all DOM elements.
 *
 * @return {ve.ui.Surface}
 * @chainable
 * @fires ve.ui.Surface#destroy
 */
ve.ui.Surface.prototype.destroy = function () {
	// Destroy the ce.Surface, the ui.Context and window managers
	this.context.destroy();
	this.dialogs.destroy();
	for ( const side in this.toolbarDialogs ) {
		this.toolbarDialogs[ side ].destroy();
	}
	this.view.destroy();
	if ( this.debugBar ) {
		this.debugBar.destroy();
	}

	// Disconnect events
	this.dialogs.disconnect( this );
	this.context.getInspectors().disconnect( this );

	// Remove DOM elements
	this.$element.remove();
	this.globalOverlay.$element.remove();

	// Let others know we have been destroyed
	this.emit( 'destroy' );

	return this;
};

/**
 * Initialize surface.
 *
 * This must be called after the surface has been attached to the DOM.
 *
 * @return {ve.ui.Surface}
 * @chainable
 * @fires ve.ui.Surface#ready
 */
ve.ui.Surface.prototype.initialize = function () {
	$( OO.ui.getTeleportTarget() ).append( this.globalOverlay.$element );

	if ( ve.debug ) {
		this.setupDebugBar();
	}

	// The following classes are used here:
	// * ve-ui-surface-dir-ltr
	// * ve-ui-surface-dir-rtl
	this.$element.addClass( 've-ui-surface-dir-' + this.getDir() );

	this.getView().initialize();
	this.getModel().initialize();

	this.emit( 'ready' );
	return this;
};

/**
 * Get the target the surface belongs to
 *
 * @return {ve.init.Target}
 */
ve.ui.Surface.prototype.getTarget = function () {
	return this.target;
};

/**
 * Get the DOM representation of the surface's current state.
 *
 * @return {HTMLDocument|string} HTML document (visual mode) or text (source mode)
 */
ve.ui.Surface.prototype.getDom = function () {
	return this.getModel().getDom();
};

/**
 * Get the HTML representation of the surface's current state.
 *
 * @return {string} HTML
 */
ve.ui.Surface.prototype.getHtml = function () {
	return this.getModel().getHtml();
};

/**
 * Get the surface's editing mode
 *
 * @return {string} Editing mode
 */
ve.ui.Surface.prototype.getMode = function () {
	return this.mode;
};

/**
 * Create a context.
 *
 * @param {Object} config Configuration options
 * @return {ve.ui.LinearContext}
 */
ve.ui.Surface.prototype.createContext = function ( config ) {
	return OO.ui.isMobile() ? new ve.ui.MobileContext( this, config ) : new ve.ui.DesktopContext( this, config );
};

/**
 * Create a dialog window manager.
 *
 * @return {ve.ui.WindowManager} Dialog window manager
 */
ve.ui.Surface.prototype.createDialogWindowManager = function () {
	return OO.ui.isMobile() ?
		new ve.ui.MobileWindowManager( this, {
			factory: ve.ui.windowFactory,
			overlay: this.globalOverlay
		} ) :
		new ve.ui.SurfaceWindowManager( this, { factory: ve.ui.windowFactory } );
};

/**
 * Create a surface model
 *
 * @param {ve.dm.Document} doc Document model
 * @param {ve.dm.BranchNode} [attachedRoot] Node to surface
 * @return {ve.dm.Surface} Surface model
 */
ve.ui.Surface.prototype.createModel = function ( doc, attachedRoot ) {
	return new ve.dm.Surface( doc, attachedRoot, { sourceMode: this.getMode() === 'source' } );
};

/**
 * Create a surface view
 *
 * @param {ve.dm.Surface} model Surface model
 * @return {ve.ce.Surface} Surface view
 */
ve.ui.Surface.prototype.createView = function ( model ) {
	return new ve.ce.Surface( model, this );
};

/**
 * Set up the debug bar and insert it into the DOM.
 */
ve.ui.Surface.prototype.setupDebugBar = function () {
	this.debugBar = new ve.ui.DebugBar( this );
	this.$element.append( this.debugBar.$element );
};

/**
 * Get the bounding rectangle of the surface, relative to the viewport.
 *
 * @return {Object|null} Object with top, bottom, left, right, width and height properties.
 *  Null if the surface is not attached.
 */
ve.ui.Surface.prototype.getBoundingClientRect = function () {
	// We would use getBoundingClientRect(), but in iOS7 that's relative to the
	// document rather than to the viewport
	return this.$element[ 0 ].getClientRects()[ 0 ] || null;
};

/**
 * Get vertical measurements of the visible area of the surface viewport
 *
 * @return {Object|null} Object with top, left, bottom, and height properties. Null if the surface is not attached.
 */
ve.ui.Surface.prototype.getViewportDimensions = function () {
	const rect = this.getBoundingClientRect();

	if ( !rect ) {
		return null;
	}

	const top = Math.max( this.getPadding().top - rect.top, 0 );
	const bottom = $( this.getElementWindow() ).height() - rect.top;

	return {
		top: top,
		left: rect.left,
		bottom: bottom,
		height: bottom - top
	};
};

/**
 * Get the surface model.
 *
 * @return {ve.dm.Surface} Surface model
 */
ve.ui.Surface.prototype.getModel = function () {
	return this.model;
};

/**
 * Get the surface view.
 *
 * @return {ve.ce.Surface} Surface view
 */
ve.ui.Surface.prototype.getView = function () {
	return this.view;
};

/**
 * Get the context menu.
 *
 * @return {ve.ui.LinearContext} Context user interface
 */
ve.ui.Surface.prototype.getContext = function () {
	return this.context;
};

/**
 * Get dialogs window set.
 *
 * @return {ve.ui.WindowManager} Dialogs window set
 */
ve.ui.Surface.prototype.getDialogs = function () {
	return this.dialogs;
};

/**
 * Get toolbar dialogs window set.
 *
 * @param {string} [position='side'] Get the toolbar dialogs window set for a specific position
 * @return {ve.ui.WindowManager} Toolbar dialogs window set
 */
ve.ui.Surface.prototype.getToolbarDialogs = function ( position ) {
	position = position || 'side';
	this.toolbarDialogs[ position ] = this.toolbarDialogs[ position ] ||
		new ve.ui.ToolbarDialogWindowManager( this, {
			factory: ve.ui.windowFactory,
			modal: false
		} );
	return this.toolbarDialogs[ position ];
};

/**
 * Get the local overlay.
 *
 * Local overlays are attached to the same frame as the surface.
 *
 * @return {ve.ui.Overlay} Local overlay
 */
ve.ui.Surface.prototype.getLocalOverlay = function () {
	return this.localOverlay;
};

/**
 * Get the global overlay.
 *
 * Global overlays are attached to the top-most frame.
 *
 * @return {ve.ui.Overlay} Global overlay
 */
ve.ui.Surface.prototype.getGlobalOverlay = function () {
	return this.globalOverlay;
};

/**
 * @inheritdoc
 */
ve.ui.Surface.prototype.setDisabled = function ( disabled ) {
	if ( disabled ) {
		OO.ui.warnDeprecation( 'Surfaces can\'t be disabled, only set to readOnly' );
	}
};

/**
 * Set the read-only state of the surface
 *
 * @param {boolean} readOnly Make surface read-only
 * @fires ve.ui.Surface#readOnly
 */
ve.ui.Surface.prototype.setReadOnly = function ( readOnly ) {
	this.readOnly = !!readOnly;
	this.model.setReadOnly( readOnly );
	this.view.setReadOnly( readOnly );
	this.emit( 'readOnly', readOnly );
};

/**
 * Check if the surface is read-only
 *
 * @return {boolean}
 */
ve.ui.Surface.prototype.isReadOnly = function () {
	return this.readOnly;
};

/**
 * Give focus to the surface
 */
ve.ui.Surface.prototype.focus = function () {
	this.getView().focus();
};

/**
 * Handle transact events from the document model
 *
 * @param {ve.dm.Transaction} Transaction
 */
ve.ui.Surface.prototype.onDocumentTransact = function () {
	if ( this.placeholder ) {
		this.updatePlaceholder();
	}
};

/**
 * Handle select events from the model
 */
ve.ui.Surface.prototype.onModelSelect = function () {
	// eslint-disable-next-line no-bitwise
	if ( this.getView().dragging ^ OO.ui.isMobile() ) {
		// Allow native scroll behavior while dragging, as the start/end
		// points are unreliable until we're finished. Without this, trying to
		// drag a selection larger than a single screen will sometimes lock
		// the viewport in place, as it tries to keep the wrong end of the
		// selection on-screen.
		// On mobile the dragging flag is essentially reversed in meaning, as
		// it is set during mouse down, which happens when you are tapping
		// to select, but when you drag selection handles no mousedown event
		// occurs (or any event other 'selectionchange') so the flag is unset.
		return;
	}
	const synchronizer = this.getModel().synchronizer;
	// Don't scroll to this user's cursor due to another user's changes being applied
	if ( !( synchronizer && synchronizer.applying ) ) {
		this.scrollSelectionIntoViewDebounced();
	}
};

/**
 * Scroll the selection into view
 *
 * Called in response to selection events.
 *
 * This is done for all selections, even native ones, to account
 * for the extra padding of the floating toolbar.
 *
 * @param {ve.dm.Selection} [selectionModel] Optional selection model, defaults to current selection
 * @param {Object} [scrollConfig] Scroll config options, passed to ve.scrollIntoView
 * @fires ve.ui.Surface#scroll
 */
ve.ui.Surface.prototype.scrollSelectionIntoView = function ( selectionModel, scrollConfig ) {
	selectionModel = selectionModel || this.getModel().getSelection();

	const view = this.getView(),
		selectionView = view.getSelection( selectionModel ),
		isNative = selectionView.isNativeCursor();

	// We only care about the focus end of the selection, the anchor never
	// moves and should be allowed off screen.
	let clientRect = selectionView.getSelectionFocusRect();
	const surfaceRect = this.getBoundingClientRect();
	if ( !clientRect || !surfaceRect ) {
		return;
	}

	// We want viewport-relative coordinates, so we need to translate it
	clientRect = ve.translateRect( clientRect, surfaceRect.left, surfaceRect.top );

	const padding = ve.copy( this.getPadding() );

	let animate = true;
	if ( isNative ) {
		animate = false;
		if (
			OO.ui.isMobile() &&
			!selectionModel.isCollapsed()
		) {
			const profile = $.client.profile();
			// Assume that if the selection has been expanded, then a context menu is visible
			// above the selection. We don't want this to obscure the toolbar so add on an
			// estimate of its height.
			// Previously we applied this fix to iOS, even though scrolling closed the context,
			// because the user could touch the selection to re-open it. However sometime between
			// iOS 12 and 12.3, scrolling stopped closing the context, but it doesn't move it either,
			// so this fix became useless.
			// Older versions of Android draw the context menu in the address bar and so
			// don't need to be fixed.
			if ( profile.name === 'android' && profile.versionNumber >= 6 ) {
				padding.top += 60;
			}
			// Also assume there are selection handles below on Android. (T204718)
			if ( profile.name === 'android' || profile.name === 'firefox' ) {
				padding.bottom += 30;
			}
		}
	} else {
		// Don't attempt to scroll non-native selections into view if they
		// are taller than the viewport (T305862).
		const viewportDimensions = this.getViewportDimensions();
		if ( clientRect.height > viewportDimensions.height ) {
			return;
		}
	}

	// Add some minimum padding so the selection doesn't touch the edge of the viewport
	padding.top += 5;
	padding.bottom += 5;
	padding.left += 5;
	padding.right += 5;

	ve.scrollIntoView( clientRect, ve.extendObject( {
		animate: animate,
		scrollContainer: this.$scrollContainer[ 0 ],
		padding: padding
	}, scrollConfig ) ).then( () => {
		if ( isNative ) {
			// TODO: This event has only even been emitted for native selection
			// scroll changes. Perhaps rename it.
			this.emit( 'scroll' );
		}
	} );
};

// Deprecated alias
ve.ui.Surface.prototype.scrollCursorIntoView = ve.ui.Surface.prototype.scrollSelectionIntoView;

/**
 * Set placeholder text
 *
 * @param {string} [placeholder] Placeholder text, clears placeholder if not set
 */
ve.ui.Surface.prototype.setPlaceholder = function ( placeholder ) {
	this.placeholder = placeholder;
	if ( this.placeholder ) {
		this.$placeholder.prependTo( this.$element );
		this.updatePlaceholder();
		const documentView = this.getView().getDocument();
		this.$placeholder.prop( {
			dir: documentView.getDir(),
			lang: documentView.getLang()
		} );
	} else {
		this.$placeholder.detach();
		this.placeholderVisible = false;
		this.getView().$element.css( 'min-height', '' );
	}
	this.getView().attachedRoot.$element.attr( 'aria-label', this.placeholder || null );
};

/**
 * Update placeholder rendering
 */
ve.ui.Surface.prototype.updatePlaceholder = function () {
	const hasContent = this.getModel().getDocument().data.hasContent();

	this.$placeholder.toggleClass( 'oo-ui-element-hidden', hasContent );
	this.placeholderVisible = !hasContent;
	if ( !hasContent ) {
		// Use a clone of the first node in the document so the placeholder
		// styling matches the text the users sees when they start typing
		const firstNode = this.getView().attachedRoot.children[ 0 ];
		let $wrapper;
		if ( firstNode ) {
			$wrapper = firstNode.$element.clone();
			if ( ve.debug ) {
				// In debug mode a background colour from the render animation may be present
				$wrapper.removeAttr( 'style' );
			}
		} else {
			$wrapper = $( '<p>' );
		}
		this.$placeholder.empty().append( $wrapper.text( this.placeholder ) );
	} else {
		this.getView().$element.css( 'min-height', '' );
	}
};

/**
 * Handle position events from the view
 *
 * @param {boolean} [wasSynchronizing]
 */
ve.ui.Surface.prototype.onViewPosition = function ( wasSynchronizing ) {
	const padding = {};
	for ( const side in this.toolbarDialogs ) {
		ve.extendObject( padding, this.toolbarDialogs[ side ].getSurfacePadding() );
	}
	if ( Object.keys( padding ).length ) {
		this.setPadding( padding );
		this.adjustVisiblePadding();
		// Don't scroll to this user's cursor due to another user's changes being applied
		if ( !wasSynchronizing ) {
			this.scrollSelectionIntoView();
		}
	}
	if ( this.placeholderVisible ) {
		this.getView().$element.css( 'min-height', this.$placeholder.outerHeight() );
	}
};

/**
 * Get list of commands available on this surface.
 *
 * @return {string[]} Commands
 */
ve.ui.Surface.prototype.getCommands = function () {
	return this.commands;
};

/**
 * Execute an action or command
 *
 * This is a compatability wrapper for executeCommandWithSource.
 *
 * @param {ve.ui.Trigger|string} triggerOrAction Trigger or symbolic name of action
 * @param {string} [method] Action method name
 * @param {...any} [args] Additional arguments for action
 * @return {boolean} Action or command was executed
 */
ve.ui.Surface.prototype.execute = function ( triggerOrAction, method, ...args ) {
	return this.executeWithSource( triggerOrAction, method, false, ...args );
};

/**
 * Execute an action or command
 *
 * @param {ve.ui.Trigger|string} triggerOrAction Trigger or symbolic name of action
 * @param {string} [method] Action method name
 * @param {string} [source] Action source, for logging
 * @param {...any} [args] Additional arguments for action
 * @return {boolean} Action or command was executed
 */
ve.ui.Surface.prototype.executeWithSource = function ( triggerOrAction, method, source, ...args ) {
	if ( triggerOrAction instanceof ve.ui.Trigger ) {
		const command = this.triggerListener.getCommandByTrigger( triggerOrAction.toString() );
		if ( command ) {
			// Have command call execute with action arguments
			return command.execute( this, false, source );
		}
	} else if ( typeof triggerOrAction === 'string' && typeof method === 'string' ) {
		// Validate method
		if ( ve.ui.actionFactory.doesActionSupportMethod( triggerOrAction, method ) ) {
			// Create an action object and execute the method on it
			const obj = ve.ui.actionFactory.create( triggerOrAction, this, source );
			const ret = obj[ method ]( ...args );
			return ret === undefined || !!ret;
		}
	}
	return false;
};

/**
 * Execute a command by name
 *
 * @param {string} commandName Command name
 * @return {boolean} The command was executed
 */
ve.ui.Surface.prototype.executeCommand = function ( commandName ) {
	const command = this.commandRegistry.lookup( commandName );
	if ( command ) {
		return command.execute( this );
	}
	return false;
};

// Deprecated, use #setPadding
ve.ui.Surface.prototype.setToolbarHeight = function ( toolbarHeight ) {
	this.setPadding( { top: toolbarHeight } );
};

/**
 * @typedef {Object} Padding
 * @memberof ve.ui.Surface
 * @property {number} [top] Top padding
 * @property {number} [right] Right padding
 * @property {number} [bottom] Bottom padding
 * @property {number} [left] Left padding
 */

/**
 * Set content area padding.
 *
 * When UI components obscure the surface (e.g. the toolbar),
 * set the appropriate amount of padding here so that
 * scroll-into-view calculations can be adjusted.
 *
 * @param {ve.ui.Surface.Padding} padding Padding object. Omit properties to leave unchanged.
 */
ve.ui.Surface.prototype.setPadding = function ( padding ) {
	ve.extendObject( this.padding, padding );
	// Deprecated, use this.padding.top
	this.toolbarHeight = this.padding.top;
};

/**
 * Get the current content area padding
 *
 * Padding in this context means areas of the surface which are
 * rendered but are obsured by some other UI element, e.g. a
 * floating toolbar (but not a static toolbar).
 *
 * This can be used when deciding how to position other floating
 * UI elements, e.g. to avoid rendering a context menu under
 * a floating toolbar.
 *
 * @return {ve.ui.Surface.Padding}
 */
ve.ui.Surface.prototype.getPadding = function () {
	return this.padding;
};

/**
 * Handle resize events from the context
 */
ve.ui.Surface.prototype.onContextResize = function () {
	const padding = this.context.getSurfacePadding();
	if ( padding ) {
		this.setPadding( padding );
		this.adjustVisiblePadding();
		this.scrollSelectionIntoView();
	}
};

/**
 * Handle surface model blur events
 */
ve.ui.Surface.prototype.onModelBlur = function () {
	this.adjustVisiblePadding();
};

/**
 * Handle surface model focus events
 */
ve.ui.Surface.prototype.onModelFocus = function () {
	this.adjustVisiblePadding();
};

/**
 * Handle surface view activation events
 */
ve.ui.Surface.prototype.onViewActivation = function () {
	this.adjustVisiblePadding();
};

/**
 * Adjust visible padding on the surface to allow the whole document
 * to be scrolled to.
 */
ve.ui.Surface.prototype.adjustVisiblePadding = function () {
	if ( OO.ui.isMobile() && !this.inTargetWidget ) {
		const keyboardShown = this.getView().hasNativeCursorSelection();
		let bottom;
		if ( ve.init.platform.constructor.static.isIos() && keyboardShown ) {
			// iOS needs a whole extra page of padding when the virtual keyboard is shown.
			// Note: we keep this padding when surface is deactivated-but-shown-as-activated
			// so that the view doesn't shift when e.g. opening a toolbar toolgroup popup.
			bottom = $( window ).height() - this.getPadding().top;
		} else {
			// otherwise just add padding to account for the context
			bottom = this.getPadding().bottom;
		}
		this.getView().$attachedRootNode.css( 'padding-bottom', bottom );
		this.scrollSelectionIntoView();
	}
};

/**
 * Create a progress bar in the progress dialog
 *
 * @param {jQuery.Promise} progressCompletePromise Promise which resolves when the progress action is complete
 * @param {jQuery|string|Function} label Progress bar label
 * @param {boolean} nonCancellable Progress item can't be cancelled
 * @return {jQuery.Promise} Promise which resolves with a progress bar widget and fails if cancelled
 */
ve.ui.Surface.prototype.createProgress = function ( progressCompletePromise, label, nonCancellable ) {
	const progressBarDeferred = ve.createDeferred();

	this.progresses.push( {
		label: label,
		cancellable: !nonCancellable,
		progressCompletePromise: progressCompletePromise,
		progressBarDeferred: progressBarDeferred
	} );

	this.showProgressDebounced();

	return progressBarDeferred.promise();
};

ve.ui.Surface.prototype.showProgress = function () {
	const progresses = this.progresses;

	this.dialogs.openWindow( 'progress', { progresses: progresses, $returnFocusTo: null } );
	this.progresses = [];
};

/**
 * Get sanitization rules for rich paste
 *
 * @return {Object} Import rules
 */
ve.ui.Surface.prototype.getImportRules = function () {
	return this.importRules;
};

/**
 * Check if the surface is multi-line
 *
 * @return {boolean} Surface is multi-line
 */
ve.ui.Surface.prototype.isMultiline = function () {
	return this.multiline;
};

/**
 * Surface 'dir' property (GUI/User-Level Direction)
 *
 * @return {string} 'ltr' or 'rtl'
 */
ve.ui.Surface.prototype.getDir = function () {
	return this.$element.css( 'direction' );
};

/**
 * Get the name of the dialog this surface is in
 *
 * @return {string} The name of the dialog this surface is in
 */
ve.ui.Surface.prototype.getInDialog = function () {
	return this.inDialog;
};