/*!
 * VisualEditor Initialization Target class.
 *
 * @copyright See AUTHORS.txt
 */

/**
 * Generic Initialization target.
 *
 * @class
 * @abstract
 * @extends OO.ui.Element
 * @mixes OO.EventEmitter
 *
 * @constructor
 * @param {Object} [config] Configuration options
 * @param {Object} [config.toolbarConfig={}] Configuration options for the toolbar
 * @param {Object} [config.toolbarGroups] Toolbar groups, defaults to this.constructor.static.toolbarGroups
 * @param {string[]} [config.modes] Available editing modes. Defaults to static.modes
 * @param {string} [config.defaultMode] Default mode for new surfaces. Must be in this.modes and defaults to first item.
 * @param {boolean} [config.register=true] Register the target at ve.init.target
 */
ve.init.Target = function VeInitTarget( config = {} ) {
	// Parent constructor
	ve.init.Target.super.call( this, config );

	// Mixin constructors
	OO.EventEmitter.call( this );

	// Register
	if ( config.register !== false ) {
		ve.init.target = this;
	}

	// Properties
	this.surfaces = [];
	this.surface = null;
	this.toolbar = null;
	this.toolbarConfig = config.toolbarConfig || {};
	this.toolbarGroups = config.toolbarGroups || this.constructor.static.toolbarGroups;
	this.$scrollContainer = this.getScrollContainer();
	this.$scrollListener = this.$scrollContainer.is( 'html, body' ) ?
		$( OO.ui.Element.static.getWindow( this.$scrollContainer[ 0 ] ) ) :
		this.$scrollContainer;

	this.toolbarScrollOffset = 0;
	this.activeToolbars = 0;
	this.wasSurfaceActive = null;
	this.teardownPromise = null;

	this.modes = config.modes || this.constructor.static.modes;
	this.setDefaultMode( config.defaultMode );

	this.setupTriggerListeners();

	// Initialization
	this.$element.addClass( 've-init-target' );

	if ( ve.init.platform.constructor.static.isIos() ) {
		this.$element.addClass( 've-init-target-ios' );
	}

	// Events
	this.onDocumentKeyDownHandler = this.onDocumentKeyDown.bind( this );
	this.onDocumentKeyUpHandler = this.onDocumentKeyUp.bind( this );
	this.onDocumentVisibilityChangeHandler = this.onDocumentVisibilityChange.bind( this );
	this.onTargetKeyDownHandler = this.onTargetKeyDown.bind( this );
	this.onContainerScrollHandler = this.onContainerScroll.bind( this );
	this.onVirtualKeyboardChangeThrottled = OO.ui.throttle( this.onVirtualKeyboardChange.bind( this ), 100 );
	this.bindHandlers();
};

/* Inheritance */

OO.inheritClass( ve.init.Target, OO.ui.Element );

OO.mixinClass( ve.init.Target, OO.EventEmitter );

/* Events */

/**
 * Must be fired after the surface is initialized
 *
 * @event ve.init.Target#surfaceReady
 */

/**
 * The virtual keyboard has been opened or closed
 *
 * @event ve.init.Target#virtualKeyboardChange
 * @param {boolean} isOpen The virtual keyboard is open
 */

/* Static Properties */

/**
 * Editing modes available in the target.
 *
 * Must contain at least one mode. Overridden if the #modes config option is used.
 *
 * @static
 * @property {string[]}
 * @inheritable
 */
ve.init.Target.static.modes = [ 'visual' ];

/**
 * Toolbar definition, passed to ve.ui.Toolbar#setup
 *
 * @static
 * @property {Array}
 * @inheritable
 */
ve.init.Target.static.toolbarGroups = [
	{
		name: 'history',
		include: [ { group: 'history' } ]
	},
	{
		name: 'format',
		header: OO.ui.deferMsg( 'visualeditor-toolbar-paragraph-format' ),
		title: OO.ui.deferMsg( 'visualeditor-toolbar-format-tooltip' ),
		type: 'menu',
		include: [ { group: 'format' } ],
		promote: [ 'paragraph' ],
		demote: [ 'preformatted', 'blockquote' ]
	},
	{
		name: 'style',
		include: [ 'bold', 'italic', 'moreTextStyle' ]
	},
	{
		name: 'link',
		include: [ 'link' ]
	},
	{
		name: 'structure',
		header: OO.ui.deferMsg( 'visualeditor-toolbar-structure' ),
		title: OO.ui.deferMsg( 'visualeditor-toolbar-structure' ),
		label: OO.ui.deferMsg( 'visualeditor-toolbar-structure' ),
		invisibleLabel: true,
		type: 'list',
		icon: 'listBullet',
		include: [ { group: 'structure' } ],
		demote: [ 'outdent', 'indent' ]
	},
	{
		name: 'insert',
		header: OO.ui.deferMsg( 'visualeditor-toolbar-insert' ),
		title: OO.ui.deferMsg( 'visualeditor-toolbar-insert' ),
		label: OO.ui.deferMsg( 'visualeditor-toolbar-insert' ),
		invisibleLabel: true,
		type: 'list',
		icon: 'add',
		include: '*'
	},
	{
		name: 'specialCharacter',
		include: [ 'specialCharacter' ]
	},
	{
		name: 'pageMenu',
		type: 'list',
		align: 'after',
		excludeFromTargetWidget: true,
		icon: 'menu',
		indicator: null,
		header: OO.ui.deferMsg( 'visualeditor-pagemenu-tooltip' ),
		title: OO.ui.deferMsg( 'visualeditor-pagemenu-tooltip' ),
		label: OO.ui.deferMsg( 'visualeditor-pagemenu-tooltip' ),
		invisibleLabel: true,
		include: [ { group: 'utility' }, { group: 'help' } ]
	}
	// ve-mw puts help in a separate group and so uses the
	// visualeditor-help-tool message.
	// TODO: Consider downstreaming this message.
];

/**
 * List of commands which can be triggered anywhere from within the document
 *
 * @type {string[]}
 */
ve.init.Target.static.documentCommands = [];

/**
 * List of commands which can be triggered from within the target element
 *
 * @type {string[]}
 */
ve.init.Target.static.targetCommands = [ 'commandHelp', 'findAndReplace', 'findNext', 'findPrevious' ];

/**
 * List of commands to include in the target
 *
 * Null means all commands in the registry are used (excluding excludeCommands)
 *
 * @type {string[]|null}
 */
ve.init.Target.static.includeCommands = null;

/**
 * List of commands to exclude from the target entirely
 *
 * @type {string[]}
 */
ve.init.Target.static.excludeCommands = [];

/**
 * Enforce interactive-widget=resizes-content
 *
 * When this target is setup, enforce interactive-widget=resizes-content
 * in the <meta name=viewport> tag.
 *
 * This should be done if the target uses a position:fixed toolbar
 * on a device which may have a virtual keyboard.
 *
 * @type {boolean}
 */
ve.init.Target.static.enforceResizesContent = false;

/**
 * Surface import rules
 *
 * One set for external (non-VE) paste sources and one for all paste sources.
 *
 * Most rules are handled in ve.dm.LinearData#sanitize, but htmlBlacklist
 * is handled in ve.ce.Surface#afterPaste.
 *
 * @type {Object}
 */
ve.init.Target.static.importRules = {
	external: {
		blacklist: {
			// Annotations
			// TODO: This also removes harmless things like <span style="font-weight: bold;">
			// which would otherwise get converted to a bold annotation
			'textStyle/span': true,
			'textStyle/font': true,
			// Nodes
			alienInline: true,
			alienBlock: true,
			alienTableCell: true,
			comment: true,
			div: true
		},
		// Selectors to filter. Runs before model type blacklist above.
		htmlBlacklist: {
			// remove: { '.selectorToRemove': true }
			unwrap: {
				fieldset: true,
				legend: true,
				// Unsupported sectioning tags
				main: true,
				nav: true,
				aside: true,
				// HTML headings are already bold by default. Some skins may use non-bold
				// heaidngs, but more likely we will end up with useless bold annotations.
				'h1 b, h2 b, h3 b, h4 b, h5 b, h6 b': true,
				'h1 strong, h2 strong, h3 strong, h4 strong, h5 strong, h6 strong': true
			}
		},
		nodeSanitization: true
	},
	all: null
};

/**
 * Allow changing focus from target surfaces with tab/shift+tab
 *
 * Prevents any ve.ui.Trigger's using these shortcuts from running.
 *
 * @type {boolean}
 */
ve.init.Target.static.allowTabFocusChange = false;

/**
 * Apply the meta/importedData annotation to pasted/dropped data
 *
 * @type {boolean}
 */
ve.init.Target.static.annotateImportedData = false;

/* Static methods */

/**
 * Create a document model from an HTML document.
 *
 * @param {HTMLDocument|string} doc HTML document or source text
 * @param {string} mode Editing mode
 * @param {Object} options Conversion options
 * @return {ve.dm.Document} Document model
 */
ve.init.Target.static.createModelFromDom = function ( doc, mode, options ) {
	if ( mode === 'source' ) {
		return ve.dm.sourceConverter.getModelFromSourceText( doc, options );
	} else {
		return ve.dm.converter.getModelFromDom( doc, options );
	}
};

/**
 * Parse document string into an HTML document
 *
 * @param {string} documentString Document. Note that this must really be a whole document
 *   with a single root tag.
 * @param {string} mode Editing mode
 * @return {HTMLDocument|string} HTML document, or document string (source mode)
 */
ve.init.Target.static.parseDocument = function ( documentString, mode ) {
	if ( mode === 'source' ) {
		return documentString;
	}
	return ve.createDocumentFromHtml( documentString );
};

// Deprecated alias
ve.init.Target.prototype.parseDocument = function () {
	return this.constructor.static.parseDocument.apply( this.constructor.static, arguments );
};

/* Methods */

/**
 * Set default editing mode for new surfaces
 *
 * @param {string} defaultMode Editing mode, see static.modes
 */
ve.init.Target.prototype.setDefaultMode = function ( defaultMode ) {
	// Mode is not available
	if ( !this.isModeAvailable( defaultMode ) ) {
		if ( !this.defaultMode ) {
			// Use default mode if nothing has been set
			defaultMode = this.modes[ 0 ];
		} else {
			// Fail if we already have a valid mode
			return;
		}
	}
	if ( defaultMode !== this.defaultMode ) {
		if ( this.defaultMode ) {
			// Documented below
			// eslint-disable-next-line mediawiki/class-doc
			this.$element.removeClass( 've-init-target-' + this.defaultMode );
		}
		// The following classes are used here:
		// * ve-init-target-visual
		// * ve-init-target-[modename]
		this.$element.addClass( 've-init-target-' + defaultMode );
		this.defaultMode = defaultMode;
	}
};

/**
 * Get default editing mode for new surfaces
 *
 * @return {string} Editing mode
 */
ve.init.Target.prototype.getDefaultMode = function () {
	return this.defaultMode;
};

/**
 * Check if a specific editing mode is available
 *
 * @param {string} mode Editing mode
 * @return {boolean} Editing mode is available
 */
ve.init.Target.prototype.isModeAvailable = function ( mode ) {
	return this.modes.includes( mode );
};

/**
 * Bind event handlers to target and document
 */
ve.init.Target.prototype.bindHandlers = function () {
	$( this.getElementDocument() ).on( {
		keydown: this.onDocumentKeyDownHandler,
		keyup: this.onDocumentKeyUpHandler,
		visibilitychange: this.onDocumentVisibilityChangeHandler
	} );
	this.$element.on( 'keydown', this.onTargetKeyDownHandler );
	this.$scrollListener[ 0 ].addEventListener( 'scroll', this.onContainerScrollHandler, { passive: true } );

	if ( 'virtualKeyboard' in navigator ) {
		$( navigator.virtualKeyboard ).on( 'geometrychange', this.onVirtualKeyboardChangeThrottled );
	} else if ( 'visualViewport' in window ) {
		this.viewportScrollContainer = OO.ui.Element.static.getClosestScrollableContainer( document.body );
		this.initialClientHeight = this.viewportScrollContainer.clientHeight;
		$( visualViewport ).on( 'resize', this.onVirtualKeyboardChangeThrottled );
	}
};

/**
 * Unbind event handlers on target and document
 */
ve.init.Target.prototype.unbindHandlers = function () {
	$( this.getElementDocument() ).off( {
		keydown: this.onDocumentKeyDownHandler,
		keyup: this.onDocumentKeyUpHandler,
		visibilitychange: this.onDocumentVisibilityChangeHandler
	} );
	this.$element.off( 'keydown', this.onTargetKeyDownHandler );
	this.$scrollListener[ 0 ].removeEventListener( 'scroll', this.onContainerScrollHandler );
	if ( 'virtualKeyboard' in navigator ) {
		$( navigator.virtualKeyboard ).off( 'geometrychange', this.onVirtualKeyboardChangeThrottled );
	} else if ( 'visualViewport' in window ) {
		$( visualViewport ).off( 'resize', this.onVirtualKeyboardChangeThrottled );
	}
};

/**
 * Teardown the target, removing all surfaces, toolbars and handlers
 *
 * @return {jQuery.Promise} Promise which resolves when the target has been torn down
 */
ve.init.Target.prototype.teardown = function () {
	if ( !this.teardownPromise ) {
		this.unbindHandlers();
		// Wait for the toolbar to teardown before clearing surfaces,
		// as it may want to transition away
		this.teardownPromise = this.teardownToolbar().then( this.clearSurfaces.bind( this ) );
	}
	return this.teardownPromise;
};

/**
 * Destroy the target
 *
 * @return {jQuery.Promise} Promise which resolves when the target has been destroyed
 */
ve.init.Target.prototype.destroy = function () {
	return this.teardown().then( () => {
		this.$element.remove();
		if ( ve.init.target === this ) {
			ve.init.target = null;
		}
	} );
};

/**
 * Set up trigger listeners
 */
ve.init.Target.prototype.setupTriggerListeners = function () {
	const surfaceOrSurfaceConfig = this.getSurface() || this.getSurfaceConfig();
	this.documentTriggerListener = new ve.TriggerListener( this.constructor.static.documentCommands, surfaceOrSurfaceConfig.commandRegistry );
	this.targetTriggerListener = new ve.TriggerListener( this.constructor.static.targetCommands, surfaceOrSurfaceConfig.commandRegistry );
};

/**
 * Get the target's scroll container
 *
 * @return {jQuery} The target's scroll container
 */
ve.init.Target.prototype.getScrollContainer = function () {
	return $( OO.ui.Element.static.getClosestScrollableContainer( document.body ) );
};

/**
 * Handle scroll container scroll events
 */
ve.init.Target.prototype.onContainerScroll = function () {
	const toolbar = this.getToolbar();

	if ( toolbar && toolbar.isFloatable() ) {
		const wasFloating = toolbar.isFloating();
		const scrollTop = this.$scrollContainer.scrollTop();

		if ( scrollTop + this.toolbarScrollOffset > toolbar.getElementOffset().top ) {
			toolbar.float();
		} else {
			toolbar.unfloat();
		}

		if ( toolbar.isFloating() !== wasFloating ) {
			// HACK: Re-position any active toolgroup popups. We can't rely on normal event handler order
			// because we're mixing jQuery and non-jQuery events. T205924#4657203
			toolbar.getItems().forEach( ( toolgroup ) => {
				if ( toolgroup instanceof OO.ui.PopupToolGroup && toolgroup.isActive() ) {
					toolgroup.position();
				}
			} );
		}
	}
};

/**
 * Handle key down events on the document
 *
 * @param {jQuery.Event} e Key down event
 */
ve.init.Target.prototype.onDocumentKeyDown = function ( e ) {
	const trigger = new ve.ui.Trigger( e );
	if ( trigger.isComplete() ) {
		const command = this.documentTriggerListener.getCommandByTrigger( trigger.toString() );
		const surface = this.getSurface();
		if ( surface && command && command.execute( surface, undefined, 'trigger' ) ) {
			e.preventDefault();
		}
	}
	// Allows elements to re-style for ctrl+click behaviour, e.g. ve.ce.LinkAnnotation
	this.$element.toggleClass( 've-init-target-ctrl-meta-down', e.ctrlKey || e.metaKey );
};

/**
 * Handle key up events on the document
 *
 * @param {jQuery.Event} e Key up event
 */
ve.init.Target.prototype.onDocumentKeyUp = function ( e ) {
	this.$element.toggleClass( 've-init-target-ctrl-meta-down', e.ctrlKey || e.metaKey );
};

/**
 * Handle visibility change events on the document
 *
 * @param {jQuery.Event} e Visibility change event
 */
ve.init.Target.prototype.onDocumentVisibilityChange = function () {
	// keyup event will be missed if you ctrl+tab away from the page
	this.$element.removeClass( 've-init-target-ctrl-meta-down' );
};

/**
 * Handle key down events on the target
 *
 * @param {jQuery.Event} e Key down event
 */
ve.init.Target.prototype.onTargetKeyDown = function ( e ) {
	const trigger = new ve.ui.Trigger( e );
	if ( trigger.isComplete() ) {
		const command = this.targetTriggerListener.getCommandByTrigger( trigger.toString() );
		const surface = this.getSurface();
		if ( surface && command && command.execute( surface, undefined, 'trigger' ) ) {
			e.preventDefault();
		}
	}
};

/**
 * Handle toolbar resize events
 */
ve.init.Target.prototype.onToolbarResize = function () {
	if ( !this.getSurface() ) {
		return;
	}
	this.getSurface().getView().emit( 'position', true );
};

/**
 * Get an object describing the amount of padding the toolbar adds to the surface.
 *
 * @return {ve.ui.Surface.Padding|null} Padding object, or null
 */
ve.init.Target.prototype.getToolbarSurfacePadding = function () {
	const toolbar = this.getToolbar();
	if ( !toolbar ) {
		return null;
	}
	return {
		top: toolbar.getHeight() + this.toolbarScrollOffset
	};
};

/**
 * Create a target widget.
 *
 * @param {Object} [config] Configuration options
 * @return {ve.ui.TargetWidget}
 */
ve.init.Target.prototype.createTargetWidget = function ( config = {} ) {
	return new ve.ui.TargetWidget( ve.extendObject( {
		toolbarGroups: this.toolbarGroups.filter( ( group ) => !group.excludeFromTargetWidget )
	}, config ) );
};

/**
 * Create a surface.
 *
 * @param {ve.dm.Document|ve.dm.Surface} dmDocOrSurface Document model or surface model
 * @param {Object} [config] Configuration options
 * @return {ve.ui.Surface}
 */
ve.init.Target.prototype.createSurface = function ( dmDocOrSurface, config = {} ) {
	return new ve.ui.Surface( this, dmDocOrSurface, this.getSurfaceConfig( config ) );
};

/**
 * Get surface configuration options
 *
 * @param {Object} config Configuration option overrides
 * @return {Object} Surface configuration options
 */
ve.init.Target.prototype.getSurfaceConfig = function ( config = {} ) {
	return ve.extendObject( {
		$scrollContainer: this.$scrollContainer,
		$scrollListener: this.$scrollListener,
		commandRegistry: ve.ui.commandRegistry,
		sequenceRegistry: ve.ui.sequenceRegistry,
		dataTransferHandlerFactory: ve.ui.dataTransferHandlerFactory,
		includeCommands: this.constructor.static.includeCommands,
		excludeCommands: OO.simpleArrayUnion(
			this.constructor.static.excludeCommands,
			this.constructor.static.documentCommands,
			this.constructor.static.targetCommands
		),
		importRules: this.constructor.static.importRules,
		allowTabFocusChange: this.constructor.static.allowTabFocusChange
	}, config );
};

/**
 * Add a surface to the target
 *
 * @param {ve.dm.Document|ve.dm.Surface} dmDocOrSurface Document model or surface model
 * @param {Object} [config] Configuration options
 * @return {ve.ui.Surface}
 */
ve.init.Target.prototype.addSurface = function ( dmDocOrSurface, config = {} ) {
	const surface = this.createSurface( dmDocOrSurface, ve.extendObject( { mode: this.getDefaultMode() }, config ) );
	this.surfaces.push( surface );
	surface.getView().connect( this, {
		focus: this.onSurfaceViewFocus.bind( this, surface )
	} );
	// Sub-classes should initialize the surface when possible, then fire 'surfaceReady'
	return surface;
};

/**
 * Destroy and remove all surfaces from the target
 */
ve.init.Target.prototype.clearSurfaces = function () {
	// We're about to destroy this.surface, so unset it for sanity
	// Otherwise, getSurface() could return a destroyed surface
	this.surface = null;
	while ( this.surfaces.length ) {
		this.surfaces.pop().destroy();
	}
};

/**
 * Handle focus events from a surface's view
 *
 * @param {ve.ui.Surface} surface Surface firing the event
 */
ve.init.Target.prototype.onSurfaceViewFocus = function ( surface ) {
	this.setSurface( surface );
};

/**
 * Set the target's active surface
 *
 * @param {ve.ui.Surface} surface
 */
ve.init.Target.prototype.setSurface = function ( surface ) {
	if ( OO.ui.isMobile() ) {
		// Allow popup tool groups's menus to display on top of the mobile context, which is attached
		// to the global overlay (T307849)
		this.toolbarConfig.$overlay = surface.getGlobalOverlay().$element;
		// There is already a toolbar (e.g. when switching), swap out the overlay:
		// TODO: Add a setOverlay method to Toolbar, or create a new toolbar
		if ( this.toolbar ) {
			this.toolbar.$overlay = this.toolbarConfig.$overlay;
			this.toolbar.$overlay.append( this.toolbar.$popups );
		}
	}

	if ( !this.surfaces.includes( surface ) ) {
		throw new Error( 'Active surface must have been added first' );
	}
	if ( surface !== this.surface ) {
		this.surface = surface;
		this.setupToolbar( surface );
	}
};

/**
 * Get the target's active surface, if it exists
 *
 * @return {ve.ui.Surface|null}
 */
ve.init.Target.prototype.getSurface = function () {
	return this.surface;
};

/**
 * Get the target's toolbar, if it exists
 *
 * @return {ve.ui.TargetToolbar|null}
 */
ve.init.Target.prototype.getToolbar = function () {
	return this.toolbar;
};

/**
 * Set up the toolbar if it doesn't exist, and attach it to a surface
 *
 * If the toolbar already exists it can still be attached
 * to another surface.
 *
 * @param {ve.ui.Surface} newSurface
 */
ve.init.Target.prototype.setupToolbar = function ( newSurface ) {
	// Create toolbar if it doesn't exist
	if ( !this.toolbar ) {
		const toolbar = this.toolbar = new ve.ui.PositionedTargetToolbar( this, this.toolbarConfig );

		if ( this.constructor.static.enforceResizesContent ) {
			this.toggleResizesContent( true );
		}

		toolbar.connect( this, {
			resize: 'onToolbarResize',
			active: 'onToolbarActive'
		} );

		toolbar.$element
			.on( 'focusin', () => {
				const surface = toolbar.getSurface();
				if ( !surface || !surface.nullSelectionOnBlur ) {
					return;
				}
				// When the focus moves to the toolbar, deactivate the surface but keep the selection (even if
				// nullSelectionOnBlur is true), to allow tools to act on that selection.
				surface.getView().deactivate( /* showAsActivated= */ true );
			} )
			.on( 'focusout', ( e ) => {
				const surface = toolbar.getSurface();
				if ( !surface || !surface.nullSelectionOnBlur ) {
					return;
				}
				const newFocusedElement = e.relatedTarget;
				if ( !OO.ui.contains( [ toolbar.$element[ 0 ], toolbar.$overlay[ 0 ] ], newFocusedElement, true ) ) {
					// When the focus moves out of the toolbar:
					if ( OO.ui.contains( surface.getView().$element[ 0 ], newFocusedElement, true ) ) {
						// When the focus moves out of the toolbar, and it moves back into the surface,
						// make sure the previous selection is restored.
						const previousSelection = surface.getModel().getSelection();
						surface.getView().activate();
						if ( !previousSelection.isNull() ) {
							surface.getModel().setSelection( previousSelection );
						}
					} else {
						// When the focus moves out of the toolbar, and it doesn't move back into the surface,
						// blur the surface explicitly to restore the expected nullSelectionOnBlur behavior.
						// The surface was deactivated, so it doesn't react to the focus change itself.
						surface.getView().blur();
					}
				}
			} );
	}

	this.toolbar.setup( this.toolbarGroups, newSurface );
	this.attachToolbar();
	requestAnimationFrame( this.onContainerScrollHandler );
};

/**
 * Deactivate the surface. Maybe save some properties that should be restored when it's activated.
 *
 * @protected
 */
ve.init.Target.prototype.deactivateSurfaceForToolbar = function () {
	const view = this.getSurface().getView();
	// Surface may already be deactivated (e.g. link inspector is open)
	this.wasSurfaceActive = !view.deactivated;
	if ( this.wasSurfaceActive ) {
		view.deactivate();
	}
};

/**
 * Activate the surface. Restore any properties saved in #deactivate.
 *
 * @protected
 */
ve.init.Target.prototype.activateSurfaceForToolbar = function () {
	const view = this.getSurface().getView();
	// For non-collapsed mobile selections, don't reactivate
	if ( this.wasSurfaceActive && !( OO.ui.isMobile() && !view.getModel().getSelection().isCollapsed() ) ) {
		view.activate();
	}
};

/**
 * Handle active events from the toolbar
 *
 * @param {boolean} active The toolbar is active
 */
ve.init.Target.prototype.onToolbarActive = function ( active ) {
	// Deactivate the surface when the toolbar is active (T109529, T201329)
	if ( active ) {
		this.activeToolbars++;
		if ( this.activeToolbars === 1 ) {
			this.deactivateSurfaceForToolbar();
		}
	} else {
		this.activeToolbars--;
		if ( !this.activeToolbars ) {
			this.activateSurfaceForToolbar();
		}
	}
};

/**
 * Teardown the toolbar
 *
 * @return {jQuery.Promise} Promise which resolves when the toolbar has been torn down
 */
ve.init.Target.prototype.teardownToolbar = function () {
	if ( this.toolbar ) {
		this.toolbar.destroy();
		this.toolbar = null;
	}
	if ( this.constructor.static.enforceResizesContent ) {
		this.toggleResizesContent( false );
	}
	return ve.createDeferred().resolve().promise();
};

/**
 * Attach the toolbar to the DOM
 */
ve.init.Target.prototype.attachToolbar = function () {
	const toolbar = this.getToolbar();
	toolbar.$element.insertBefore( toolbar.getSurface().$element );
	toolbar.initialize();
};

/**
 * Toggle "resizes content" mode of the viewport
 *
 * See #static-enforceResizesContent
 *
 * @param {boolean} set Set (true) or restore initial value (false)
 */
ve.init.Target.prototype.toggleResizesContent = function ( set ) {
	// eslint-disable-next-line no-jquery/no-global-selector
	let $viewportTag = $( 'meta[name=viewport]' );
	if ( !$viewportTag.length ) {
		$viewportTag = $( '<meta>' ).attr( 'name', 'viewport' ).appendTo( 'head' );
	}
	const obj = Object.create( null );
	( $viewportTag.attr( 'content' ) || '' ).split( /[,;]/g ).forEach( ( pair ) => {
		const [ key, value ] = pair.split( '=' );
		obj[ key.trim().toLowerCase() ] = value.trim();
	} );

	if ( set && !this.interactiveWidgetValueSet ) {
		this.initialInteractiveWidgetValue = obj[ 'interactive-widget' ];
		obj[ 'interactive-widget' ] = 'resizes-content';
		this.interactiveWidgetValueSet = true;
	} else if ( !set && this.interactiveWidgetValueSet ) {
		if ( this.initialInteractiveWidgetValue ) {
			obj[ 'interactive-widget' ] = this.initialInteractiveWidgetValue;
		} else {
			delete obj[ 'interactive-widget' ];
		}
		this.interactiveWidgetValueSet = false;
	}

	$viewportTag.attr( 'content', Object.keys( obj ).map( ( key ) => key + '=' + obj[ key ] ).join( ',' ) );
};

/**
 * Handle virtual keyboard geometry change events.
 *
 * @fires ve.init.Target#virtualKeyboardChange
 */
ve.init.Target.prototype.onVirtualKeyboardChange = function () {
	const virtualKeyboardOpen = this.isVirtualKeyboardOpen();
	if ( virtualKeyboardOpen !== this.virtualKeyboardOpen ) {
		this.virtualKeyboardOpen = virtualKeyboardOpen;
		this.emit( 'virtualKeyboardChange', virtualKeyboardOpen );
	}
};

/**
 * Check if a virtual keyboard is open
 *
 * @return {boolean} Whether a keyboard is open
 */
ve.init.Target.prototype.isVirtualKeyboardOpen = function () {
	if ( 'virtualKeyboard' in navigator ) {
		// The VirtualKeyboard API is available. It has limited browser
		// support and is only available on HTTPS, but has exactly the
		// information we need.
		return navigator.virtualKeyboard.boundingRect && navigator.virtualKeyboard.boundingRect.height > 0;
	}
	if ( !OO.ui.isMobile() ) {
		// We let VirtualKeyboard go first before abandoning for non-mobile,
		// because it should hopefully cover desktop cases as well when they
		// crop up. After this point we have to make mobile-device specific
		// assumptions.
		return false;
	}
	if ( 'visualViewport' in window ) {
		// The VisualViewport API is available. This is much more widely
		// supported, but requires us to start guessing.
		if ( ve.init.platform.constructor.static.isIos() ) {
			return visualViewport.height < this.viewportScrollContainer.clientHeight;
		}
		// TODO: Support orientation changes?
		return this.viewportScrollContainer.clientHeight < this.initialClientHeight;
	} else if ( this.getSurface() ) {
		// Fallback: assume that if there's a native selection the keyboard must
		// be open. This isn't necessarily true, but is an okay approximation for
		// our final fallback check.
		// TODO: Check if selection is in a form input?
		return this.getSurface().getView().hasNativeCursorSelection();
	}
	return false;
};
