/*!
 * VisualEditor user interface MWMediaDialog class.
 *
 * @copyright See AUTHORS.txt
 * @license The MIT License (MIT); see LICENSE.txt
 */

/**
 * Dialog for inserting and editing MediaWiki media.
 *
 * @class
 * @extends ve.ui.NodeDialog
 *
 * @constructor
 * @param {Object} [config] Configuration options
 */
ve.ui.MWMediaDialog = function VeUiMWMediaDialog( config ) {
	// Parent constructor
	ve.ui.MWMediaDialog.super.call( this, config );

	// Properties
	this.imageModel = null;
	this.isSettingUpModel = false;
	this.isInsertion = false;
	this.selectedImageInfo = null;
	this.searchCache = {};

	this.$element.addClass( 've-ui-mwMediaDialog' );
};

/* Inheritance */

OO.inheritClass( ve.ui.MWMediaDialog, ve.ui.NodeDialog );

/* Static Properties */

ve.ui.MWMediaDialog.static.name = 'media';

ve.ui.MWMediaDialog.static.title =
	OO.ui.deferMsg( 'visualeditor-dialog-media-title' );

ve.ui.MWMediaDialog.static.size = 'medium';

ve.ui.MWMediaDialog.static.actions = [
	{
		action: 'done',
		label: OO.ui.deferMsg( 'visualeditor-dialog-action-apply' ),
		flags: [ 'progressive', 'primary' ],
		modes: 'edit'
	},
	{
		action: 'insert',
		label: OO.ui.deferMsg( 'visualeditor-dialog-action-insert' ),
		flags: [ 'primary', 'progressive' ],
		modes: 'insert'
	},
	{
		action: 'change',
		label: OO.ui.deferMsg( 'visualeditor-dialog-media-change-image' ),
		modes: [ 'edit', 'insert' ]
	},
	{
		action: 'choose',
		label: OO.ui.deferMsg( 'visualeditor-dialog-media-choose-image' ),
		flags: [ 'primary', 'progressive' ],
		modes: [ 'info' ]
	},
	{
		action: 'upload',
		label: OO.ui.deferMsg( 'visualeditor-dialog-media-upload' ),
		flags: [ 'primary', 'progressive' ],
		modes: [ 'upload-upload' ]
	},
	{
		action: 'save',
		label: OO.ui.deferMsg( 'visualeditor-dialog-media-save' ),
		flags: [ 'primary', 'progressive' ],
		modes: [ 'upload-info' ]
	},
	{
		action: 'cancelchoose',
		label: OO.ui.deferMsg( 'visualeditor-dialog-media-goback' ),
		flags: [ 'safe', 'back' ],
		modes: [ 'info' ]
	},
	{
		action: 'cancelupload',
		label: OO.ui.deferMsg( 'visualeditor-dialog-media-goback' ),
		flags: [ 'safe', 'back' ],
		modes: [ 'upload-info' ]
	},
	{
		label: OO.ui.deferMsg( 'visualeditor-dialog-action-cancel' ),
		flags: [ 'safe', 'close' ],
		modes: [ 'readonly', 'edit', 'insert', 'select', 'search', 'upload-upload' ]
	},
	{
		action: 'back',
		label: OO.ui.deferMsg( 'visualeditor-dialog-media-goback' ),
		flags: [ 'safe', 'back' ],
		modes: [ 'change' ]
	}
];

ve.ui.MWMediaDialog.static.modelClasses = [ ve.dm.MWBlockImageNode, ve.dm.MWInlineImageNode ];

ve.ui.MWMediaDialog.static.includeCommands = null;

ve.ui.MWMediaDialog.static.excludeCommands = [
	// No formatting
	'paragraph',
	'heading1',
	'heading2',
	'heading3',
	'heading4',
	'heading5',
	'heading6',
	'preformatted',
	'blockquote',
	// TODO: Decide if tables tools should be allowed
	'tableCellHeader',
	'tableCellData',
	// No structure
	'bullet',
	'bulletWrapOnce',
	'number',
	'numberWrapOnce',
	'indent',
	'outdent'
];

/**
 * Get the import rules for the surface widget in the dialog
 *
 * @see ve.dm.ElementLinearData#sanitize
 * @return {Object} Import rules
 */
ve.ui.MWMediaDialog.static.getImportRules = function () {
	const rules = ve.copy( ve.init.target.constructor.static.importRules );
	return ve.extendObject(
		rules,
		{
			all: {
				blacklist: ve.extendObject(
					{
						// Tables (but not lists) are possible in wikitext with a leading
						// line break but we prevent creating these with the UI
						list: true,
						listItem: true,
						definitionList: true,
						definitionListItem: true,
						table: true,
						tableCaption: true,
						tableSection: true,
						tableRow: true,
						tableCell: true,
						mwTable: true,
						mwTransclusionTableCell: true
					},
					ve.getProp( rules, 'all', 'blacklist' )
				),
				// Headings are also possible, but discouraged
				conversions: ve.extendObject(
					{
						mwHeading: 'paragraph'
					},
					ve.getProp( rules, 'all', 'conversions' )
				)
			}
		}
	);
};

/* Methods */

/**
 * @inheritdoc
 */
ve.ui.MWMediaDialog.prototype.getBodyHeight = function () {
	// FIXME: This should vary on panel.
	return 600;
};

/**
 * @inheritdoc
 */
ve.ui.MWMediaDialog.prototype.initialize = function () {
	// Parent method
	ve.ui.MWMediaDialog.super.prototype.initialize.call( this );

	// Main layout
	this.panels = new OO.ui.StackLayout();

	// Settings panels
	this.mediaSettingsLayout = new OO.ui.IndexLayout( {
		classes: [ 've-ui-mwMediaDialog-panel-settings' ]
	} );
	this.generalSettingsPanel = new OO.ui.TabPanelLayout( 'general', {
		label: ve.msg( 'visualeditor-dialog-media-page-general' )
	} );
	this.advancedSettingsPanel = new OO.ui.TabPanelLayout( 'advanced', {
		label: ve.msg( 'visualeditor-dialog-media-page-advanced' )
	} );

	// General settings panel

	// Filename
	this.filenameFieldset = new OO.ui.FieldsetLayout( {
		label: ve.msg( 'visualeditor-dialog-media-content-filename' ),
		icon: 'image'
	} );

	// Caption
	this.captionTarget = ve.init.target.createTargetWidget( {
		includeCommands: this.constructor.static.includeCommands,
		excludeCommands: this.constructor.static.excludeCommands,
		importRules: this.constructor.static.getImportRules(),
		inDialog: this.constructor.static.name,
		multiline: false
	} );
	const captionField = new OO.ui.FieldLayout( this.captionTarget, {
		align: 'top'
	} );
	this.captionFieldset = new OO.ui.FieldsetLayout( {
		$overlay: this.$overlay,
		label: ve.msg( 'visualeditor-dialog-media-content-section' ),
		help: ve.msg( 'visualeditor-dialog-media-content-section-help' ),
		classes: [ 've-ui-mwMediaDialog-caption-fieldset' ]
	} );
	this.captionFieldset.addItems( [ captionField ] );

	// Alt text
	this.altTextInput = new OO.ui.MultilineTextInputWidget( {
		spellcheck: true,
		classes: [ 've-ui-mwMediaDialog-altText' ],
		autosize: true,
		rows: 1,
		allowLinebreaks: false
	} );
	const altTextField = new OO.ui.FieldLayout( this.altTextInput, {
		align: 'top'
	} );
	const altTextFieldset = new OO.ui.FieldsetLayout( {
		$overlay: this.$overlay,
		label: ve.msg( 'visualeditor-dialog-media-alttext-section' ),
		help: ve.msg( 'visualeditor-dialog-media-alttext-section-help' )
	} );
	altTextFieldset.addItems( [ altTextField ] );

	// Advanced settings

	// Position
	this.positionSelect = new ve.ui.AlignWidget( {
		dir: this.getDir()
	} );
	const positionSelectField = new OO.ui.FieldLayout( this.positionSelect );
	this.positionCheckbox = new OO.ui.CheckboxInputWidget();
	const positionCheckboxField = new OO.ui.FieldLayout( this.positionCheckbox, {
		$overlay: this.$overlay,
		align: 'inline',
		label: ve.msg( 'visualeditor-dialog-media-position-checkbox' ),
		help: ve.msg( 'visualeditor-dialog-media-position-checkbox-help' )
	} );
	const positionFieldset = new OO.ui.FieldsetLayout( {
		$overlay: this.$overlay,
		label: ve.msg( 'visualeditor-dialog-media-position-section' ),
		help: ve.msg( 'visualeditor-dialog-media-position-section-help' )
	} );
	positionFieldset.addItems( [
		positionCheckboxField,
		positionSelectField
	] );

	// Type
	this.typeSelectDropdown = new OO.ui.DropdownWidget( { $overlay: this.$overlay } );
	this.typeSelect = this.typeSelectDropdown.getMenu();
	this.typeSelect.addItems( [
		// TODO: Inline images require a bit of further work, will be coming soon
		new OO.ui.MenuOptionWidget( {
			data: 'thumb',
			icon: 'imageLayoutThumbnail',
			label: ve.msg( 'visualeditor-dialog-media-type-thumb' )
		} ),
		new OO.ui.MenuOptionWidget( {
			data: 'frameless',
			icon: 'imageLayoutFrameless',
			label: ve.msg( 'visualeditor-dialog-media-type-frameless' )
		} ),
		new OO.ui.MenuOptionWidget( {
			data: 'frame',
			icon: 'imageLayoutFrame',
			label: ve.msg( 'visualeditor-dialog-media-type-frame' )
		} ),
		new OO.ui.MenuOptionWidget( {
			data: 'none',
			icon: 'imageLayoutBasic',
			label: ve.msg( 'visualeditor-dialog-media-type-none' )
		} )
	] );
	const typeSelectField = new OO.ui.FieldLayout( this.typeSelectDropdown, {
		align: 'top'
	} );
	this.borderCheckbox = new OO.ui.CheckboxInputWidget();
	const borderField = new OO.ui.FieldLayout( this.borderCheckbox, {
		align: 'inline',
		label: ve.msg( 'visualeditor-dialog-media-type-border' )
	} );
	this.typeFieldset = new OO.ui.FieldsetLayout( {
		$overlay: this.$overlay,
		label: ve.msg( 'visualeditor-dialog-media-type-section' ),
		help: ve.msg( 'visualeditor-dialog-media-type-section-help' )
	} );
	this.typeFieldset.addItems( [
		typeSelectField,
		borderField
	] );

	// Size
	this.sizeWidget = new ve.ui.MediaSizeWidget( undefined, {
		dimensionsAlign: 'top'
	} );
	const sizeWidgetField = new OO.ui.FieldLayout( this.sizeWidget );
	this.sizeFieldset = new OO.ui.FieldsetLayout( {
		$overlay: this.$overlay,
		label: ve.msg( 'visualeditor-dialog-media-size-section' ),
		help: ve.msg( 'visualeditor-dialog-media-size-section-help' )
	} );
	this.sizeFieldset.addItems( [
		sizeWidgetField
	] );

	// Search, upload and info layouts
	this.mediaSearchPanel = new OO.ui.TabPanelLayout( {
		classes: [ 've-ui-mwMediaDialog-panel-search' ],
		scrollable: true
	} );
	if ( mw.ForeignStructuredUpload && mw.ForeignStructuredUpload.BookletLayout ) {
		this.mediaUploadBooklet = new mw.ForeignStructuredUpload.BookletLayout( {
			$overlay: this.$overlay
		} );
	}
	this.mediaImageInfoPanel = new OO.ui.TabPanelLayout( {
		classes: [ 've-ui-mwMediaDialog-panel-imageinfo' ],
		scrollable: false
	} );
	this.$infoPanelWrapper = $( '<div>' ).addClass( 've-ui-mwMediaDialog-panel-imageinfo-wrapper' );

	// Search and upload panels
	this.searchTabs = new OO.ui.IndexLayout();
	const searchPanel = new OO.ui.TabPanelLayout( 'search', {
		label: ve.msg( 'visualeditor-dialog-media-search-tab-search' )
	} );
	let uploadPanel;
	if ( this.mediaUploadBooklet ) {
		uploadPanel = new OO.ui.TabPanelLayout( 'upload', {
			label: ve.msg( 'visualeditor-dialog-media-search-tab-upload' ),
			content: [ this.mediaUploadBooklet ]
		} );
	}

	// Search widget
	this.search = new mw.widgets.MediaSearchWidget( {
		rowHeight: OO.ui.isMobile() ? 120 : 200
	} );

	// Events
	this.positionCheckbox.connect( this, { change: 'onPositionCheckboxChange' } );
	this.borderCheckbox.connect( this, { change: 'onBorderCheckboxChange' } );
	this.positionSelect.connect( this, { choose: 'onPositionSelectChoose' } );
	this.typeSelect.connect( this, { choose: 'onTypeSelectChoose' } );
	this.search.getQuery().connect( this, { change: 'onSearchQueryChange' } );
	this.search.getQuery().$indicator.on( 'mousedown', this.onSearchQueryClear.bind( this ) );
	this.search.getResults().connect( this, { choose: 'onSearchResultsChoose' } );
	this.captionTarget.connect( this, { change: 'checkChanged' } );
	this.altTextInput.connect( this, { change: 'onAlternateTextChange' } );
	this.searchTabs.connect( this, { set: 'onSearchTabsSet' } );
	if ( this.mediaUploadBooklet ) {
		this.mediaUploadBooklet.connect( this, {
			set: 'onMediaUploadBookletSet',
			uploadValid: 'onUploadValid',
			infoValid: 'onInfoValid'
		} );
	}

	// Append panels
	searchPanel.$element.append( this.search.$element );
	this.searchTabs.addTabPanels( [ searchPanel ] );
	if ( this.mediaUploadBooklet ) {
		this.searchTabs.addTabPanels( [ uploadPanel ] );
	}
	this.mediaSearchPanel.$element.append(
		this.searchTabs.$element
	);
	this.generalSettingsPanel.$element.append(
		this.filenameFieldset.$element,
		this.captionFieldset.$element,
		altTextFieldset.$element
	);
	this.advancedSettingsPanel.$element.append(
		positionFieldset.$element,
		this.typeFieldset.$element,
		this.sizeFieldset.$element
	);
	this.mediaSettingsLayout.addTabPanels( [
		this.generalSettingsPanel,
		this.advancedSettingsPanel
	] );
	this.panels.addItems( [
		this.mediaSearchPanel,
		this.mediaImageInfoPanel,
		this.mediaSettingsLayout
	] );
	this.$body.append( this.panels.$element );
};

/**
 * Handle set events from the search tabs
 *
 * @param {OO.ui.TabPanelLayout} tabPanel Current tabPanel
 */
ve.ui.MWMediaDialog.prototype.onSearchTabsSet = function ( tabPanel ) {
	const name = tabPanel.getName();

	this.actions.setMode( name );

	switch ( name ) {
		case 'search':
			this.setSize( 'larger' );
			break;

		case 'upload':
			// Initialize and reset the upload booklet if it hasn't
			// been initiailized since setup.
			if ( !this.mediaUploadBookletInit ) {
				this.mediaUploadBookletInit = true;
				this.mediaUploadBooklet.initialize();
			}
			this.setSize( 'medium' );
			this.uploadPageNameSet( 'upload' );
			break;
	}
};

/**
 * Handle panelNameSet events from the upload stack
 *
 * @param {OO.ui.PageLayout} page Current page
 */
ve.ui.MWMediaDialog.prototype.onMediaUploadBookletSet = function ( page ) {
	this.uploadPageNameSet( page.getName() );
};

/**
 * The upload booklet's page name has changed
 *
 * @param {string} pageName Page name
 */
ve.ui.MWMediaDialog.prototype.uploadPageNameSet = function ( pageName ) {
	if ( pageName === 'insert' ) {
		const imageInfo = this.mediaUploadBooklet.upload.getImageInfo();
		this.chooseImageInfo( imageInfo );
	} else {
		// Hide the tabs after the first page
		this.searchTabs.toggleMenu( pageName === 'upload' );

		this.actions.setMode( 'upload-' + pageName );
	}
};

/**
 * Handle uploadValid events
 *
 * @param {boolean} isValid The panel is complete and valid
 */
ve.ui.MWMediaDialog.prototype.onUploadValid = function ( isValid ) {
	this.actions.setAbilities( { upload: isValid } );
};

/**
 * Handle infoValid events
 *
 * @param {boolean} isValid The panel is complete and valid
 */
ve.ui.MWMediaDialog.prototype.onInfoValid = function ( isValid ) {
	this.actions.setAbilities( { save: isValid } );
};

/**
 * Build the image info panel from the information in the API.
 * Use the metadata info if it exists.
 * Note: Some information in the metadata object needs to be safely
 * stripped from its html wrappers.
 *
 * @param {Object} imageinfo Image info
 */
ve.ui.MWMediaDialog.prototype.buildMediaInfoPanel = function ( imageinfo ) {
	const contentDirection = this.getFragment().getDocument().getDir(),
		imageTitleText = imageinfo.title || imageinfo.canonicaltitle,
		imageTitle = new OO.ui.LabelWidget( {
			label: mw.Title.newFromText( imageTitleText ).getNameText()
		} ),
		metadata = imageinfo.extmetadata,
		// Field configuration (in order)
		apiDataKeysConfig = [
			{
				name: 'ImageDescription',
				value: ve.getProp( metadata, 'ImageDescription', 'value' ),
				format: 'html',
				view: {
					type: 'description',
					primary: true,
					descriptionHeight: '5em'
				}
			},
			{
				name: '$fileDetails',
				// Real value is set later
				value: '',
				format: 'html',
				view: { icon: 'image' }
			},
			{
				name: 'LicenseShortName',
				value: ve.getProp( metadata, 'LicenseShortName', 'value' ),
				format: 'html-remove-formatting',
				view: {
					href: ve.getProp( metadata, 'LicenseUrl', 'value' ),
					icon: this.getLicenseIcon( ve.getProp( metadata, 'LicenseShortName', 'value' ) )
				}
			},
			{
				name: 'Artist',
				value: ve.getProp( metadata, 'Artist', 'value' ),
				format: 'html-remove-formatting',
				view: {
					// "Artist" label
					labelMsg: 'visualeditor-dialog-media-info-meta-artist',
					icon: 'userAvatar'
				}
			},
			{
				name: 'Credit',
				value: ve.getProp( metadata, 'Credit', 'value' ),
				format: 'html-remove-formatting',
				view: { icon: 'userAvatar' }
			},
			{
				name: 'user',
				value: imageinfo.user,
				format: 'plaintext',
				view: {
					icon: 'userAvatar',
					// This is 'uploaded by'
					labelMsg: 'visualeditor-dialog-media-info-artist'
				}
			},
			{
				name: 'timestamp',
				value: imageinfo.timestamp,
				format: 'plaintext',
				view: {
					icon: 'clock',
					labelMsg: 'visualeditor-dialog-media-info-uploaded',
					isDate: true
				}
			},
			{
				name: 'DateTimeOriginal',
				value: ve.getProp( metadata, 'DateTimeOriginal', 'value' ),
				format: 'html-remove-formatting',
				view: {
					icon: 'clock',
					labelMsg: 'visualeditor-dialog-media-info-created'
				}
			},
			{
				name: 'moreinfo',
				value: ve.msg( 'visualeditor-dialog-media-info-moreinfo' ),
				format: 'plaintext',
				view: {
					icon: 'info',
					href: imageinfo.descriptionurl
				}
			}
		],
		fields = {},
		// Store clean API data
		apiData = {},
		fileType = this.getFileType( imageinfo.url ),
		$thumbContainer = $( '<div>' )
			.addClass( 've-ui-mwMediaDialog-panel-imageinfo-thumb' ),
		$main = $( '<div>' )
			.addClass( 've-ui-mwMediaDialog-panel-imageinfo-main' ),
		$details = $( '<div>' )
			.addClass( 've-ui-mwMediaDialog-panel-imageinfo-details' ),
		$image = $( '<img>' );

	// Main section - title
	$main.append(
		imageTitle.$element
			.addClass( 've-ui-mwMediaDialog-panel-imageinfo-title' )
	);

	// Clean data from the API responses
	for ( let i = 0; i < apiDataKeysConfig.length; i++ ) {
		const field = apiDataKeysConfig[ i ].name;
		if ( apiDataKeysConfig[ i ].format === 'html' ) {
			apiData[ field ] = new OO.ui.HtmlSnippet( apiDataKeysConfig[ i ].value );

		} else if ( apiDataKeysConfig[ i ].format === 'html-remove-formatting' ) {
			apiData[ field ] = this.cleanAPIresponse( apiDataKeysConfig[ i ].value );

		} else if ( apiDataKeysConfig[ i ].format === 'plaintext' ) {
			apiData[ field ] = apiDataKeysConfig[ i ].value;

		} else {
			throw new Error( 'Unexpected metadata field format' );
		}
	}

	// Add sizing info for non-audio images
	if ( imageinfo.mediatype === 'AUDIO' ) {
		// Label this file as an audio
		apiData.$fileDetails = $( '<span>' )
			.text( ve.msg( 'visualeditor-dialog-media-info-audiofile' ) );
	} else {
		// Build the display for image size and type
		apiData.$fileDetails = $( '<div>' )
			.append(
				$( '<span>' ).text(
					imageinfo.width +
					'\u00a0' +
					ve.msg( 'visualeditor-dimensionswidget-times' ) +
					'\u00a0' +
					imageinfo.height +
					ve.msg( 'visualeditor-dimensionswidget-px' )
				),
				$( '<span>' )
					.addClass( 've-ui-mwMediaDialog-panel-imageinfo-separator' )
					.text( ve.msg( 'visualeditor-dialog-media-info-separator' ) ),
				$( '<span>' ).text( fileType )
			);
	}

	// Attach all fields in order
	for ( let i = 0; i < apiDataKeysConfig.length; i++ ) {
		const field = apiDataKeysConfig[ i ].name;
		if ( apiData[ field ] ) {
			const $section = apiDataKeysConfig[ i ].view.primary ? $main : $details;

			fields[ field ] = new ve.ui.MWMediaInfoFieldWidget( apiData[ field ], apiDataKeysConfig[ i ].view );
			$section.append( fields[ field ].$element );
		}
	}

	// Build the info panel
	const $info = $( '<div>' )
		.addClass( 've-ui-mwMediaDialog-panel-imageinfo-info' )
		.append(
			$main.prop( 'dir', contentDirection ),
			$details
		);
	ve.targetLinksToNewWindow( $info[ 0 ] );

	// Initialize thumb container
	$thumbContainer
		.append( $image.prop( 'src', imageinfo.thumburl ) );

	this.$infoPanelWrapper.append(
		$thumbContainer,
		$info
	);

	// Force a scrollbar to the screen before we measure it
	this.mediaImageInfoPanel.$element.css( 'overflow-y', 'scroll' );
	const windowWidth = this.mediaImageInfoPanel.$element.width();

	// Define thumbnail size
	let newDimensions;
	if ( imageinfo.mediatype === 'AUDIO' ) {
		// HACK: We are getting the wrong information from the
		// API about audio files. Set their thumbnail to square
		newDimensions = {
			width: imageinfo.thumbwidth,
			height: imageinfo.thumbwidth
		};
	} else {
		// For regular images, calculate a bigger image dimensions
		newDimensions = ve.dm.MWImageNode.static.resizeToBoundingBox(
			// Original image dimensions
			{
				width: imageinfo.width,
				height: imageinfo.height
			},
			// Bounding box -- the size of the dialog, minus padding
			{
				width: windowWidth,
				height: this.getBodyHeight() - 120
			}
		);
	}
	// Resize the image
	$image.css( {
		width: newDimensions.width,
		height: newDimensions.height
	} );

	// Call for a bigger image
	this.fetchThumbnail( imageTitleText, newDimensions )
		.done( ( thumburl ) => {
			if ( thumburl ) {
				$image.prop( 'src', thumburl );
			}
		} );

	const isPortrait = newDimensions.width < ( windowWidth * 3 / 5 );
	this.mediaImageInfoPanel.$element.toggleClass( 've-ui-mwMediaDialog-panel-imageinfo-portrait', isPortrait );
	this.mediaImageInfoPanel.$element.append( this.$infoPanelWrapper );
	if ( isPortrait ) {
		$info.outerWidth( Math.floor( windowWidth - $thumbContainer.outerWidth( true ) - 15 ) );
	}

	// Initialize fields
	for ( const field in fields ) {
		fields[ field ].initialize();
	}
	// Let the scrollbar appear naturally if it should
	this.mediaImageInfoPanel.$element.css( 'overflow', '' );
};

/**
 * Fetch a bigger image thumbnail from the API.
 *
 * @param {string} imageName Image source
 * @param {Object} dimensions Image dimensions
 * @return {jQuery.Promise} Thumbnail promise that resolves with new thumb url
 */
ve.ui.MWMediaDialog.prototype.fetchThumbnail = function ( imageName, dimensions ) {
	// Check cache first
	if ( this.searchCache[ imageName ] ) {
		return ve.createDeferred().resolve( this.searchCache[ imageName ] );
	}

	const params = {
		action: 'query',
		prop: 'imageinfo',
		iiprop: 'url',
		titles: imageName
	};

	if ( dimensions.width ) {
		params.iiurlwidth = dimensions.width;
	}
	if ( dimensions.height ) {
		params.iiurlheight = dimensions.height;
	}
	return ve.init.target.getContentApi( this.getFragment().getDocument() ).get( params )
		.then( ( response ) => {
			const thumburl = ve.getProp( response.query.pages[ 0 ], 'imageinfo', 0, 'thumburl' );
			// Cache
			this.searchCache[ imageName ] = thumburl;
			return thumburl;
		} );
};

/**
 * Clean the API responses and return it in plaintext. If needed, truncate.
 *
 * @param {string} html Raw response from the API
 * @return {string} Plaintext clean response
 */
ve.ui.MWMediaDialog.prototype.cleanAPIresponse = function ( html ) {
	let text = $( $.parseHTML( html ) ).text();

	// Check if the string should be truncated
	const charLimit = 50;
	if ( text.length > charLimit ) {
		const ellipsis = ve.msg( 'visualeditor-dialog-media-info-ellipsis' );
		text = text.slice( 0, charLimit ) + ellipsis;
	}

	return text;
};

/**
 * Get the file type from the suffix of the url
 *
 * @param {string} url Full file url
 * @return {string} File type
 */
ve.ui.MWMediaDialog.prototype.getFileType = function ( url ) {
	// TODO: Validate these types, and work with icons
	// SVG, PNG, JPEG, GIF, TIFF, XCF;
	// OGA, OGG, MIDI, WAV;
	// WEBM, OGV, OGX;
	// APNG;
	// PDF, DJVU
	return url.split( '.' ).pop().toUpperCase();
};

/**
 * Get the proper icon for the license if it is recognized
 * or general info icon if it is not recognized.
 *
 * @param {string} license License short name
 * @return {string} Icon name
 */
ve.ui.MWMediaDialog.prototype.getLicenseIcon = function ( license ) {
	if ( !license ) {
		return 'info';
	}

	const normalized = license.toLowerCase().replace( /[-_]/g, ' ' );

	// FIXME: Structured data from Commons will make this properly
	// multilingual. For now, this is the limit of what is sensible.
	if ( /^((cc )?pd|public domain)/.test( normalized ) ) {
		return 'public-domain';
	} else if ( /^cc (by|sa)?/.test( normalized ) ) {
		return 'logoCC';
	} else {
		return 'info';
	}
};

/**
 * Handle search results choose event.
 *
 * @param {mw.widgets.MediaResultWidget} item Chosen item
 */
ve.ui.MWMediaDialog.prototype.onSearchResultsChoose = function ( item ) {
	this.chooseImageInfo( item.getData() );

	ve.track( 'activity.' + this.constructor.static.name, {
		action: 'search-choose-image'
	} );
};

/**
 * Handle query change events from the search input widget
 *
 * @param {string} query
 */
ve.ui.MWMediaDialog.prototype.onSearchQueryChange = function ( query ) {
	if ( query === '' ) {
		return;
	}

	ve.track( 'activity.' + this.constructor.static.name, {
		action: 'search-change-query'
	} );
};

/**
 * Handle clearing of search query by user clicking on indicator
 */
ve.ui.MWMediaDialog.prototype.onSearchQueryClear = function () {
	ve.track( 'activity.' + this.constructor.static.name, {
		action: 'search-clear-query'
	} );
};

/**
 * Choose image info for editing
 *
 * @param {Object} info Image info
 */
ve.ui.MWMediaDialog.prototype.chooseImageInfo = function ( info ) {
	this.$infoPanelWrapper.empty();
	// Switch panels
	this.selectedImageInfo = info;
	this.switchPanels( 'imageInfo' );
	// Build info panel
	this.buildMediaInfoPanel( info );
};

/**
 * Handle new image being chosen.
 *
 * @param {mw.widgets.MediaResultWidget|null} item Selected item
 */
ve.ui.MWMediaDialog.prototype.confirmSelectedImage = function () {
	const obj = {},
		info = this.selectedImageInfo;

	if ( info ) {
		const imageTitleText = info.title || info.canonicaltitle;
		// Run title through mw.Title so the File: prefix is localised
		const title = mw.Title.newFromText( imageTitleText ).getPrefixedText();
		if ( !this.imageModel ) {
			// Create a new image model based on default attributes
			this.imageModel = ve.dm.MWImageModel.static.newFromImageAttributes(
				{
					// Per https://www.mediawiki.org/w/?diff=931265&oldid=prev
					href: './' + title,
					src: info.url,
					resource: './' + title,
					width: info.thumbwidth,
					height: info.thumbheight,
					mediaType: info.mediatype,
					type: 'thumb',
					align: 'default',
					defaultSize: true,
					imageClassAttr: 'mw-file-element'
				},
				this.getFragment().getDocument()
			);
			this.attachImageModel();
			this.resetCaption();
		} else {
			// Update the current image model with the new image source
			this.imageModel.changeImageSource(
				{
					mediaType: info.mediatype,
					href: './' + title,
					src: info.url,
					resource: './' + title
				},
				info
			);
			this.updateFilenameFieldset();
		}

		// Cache
		// We're trimming the stored data down to be consistent with what
		// ImageInfoCache.getRequestPromise fetches.
		obj[ imageTitleText ] = {
			size: info.size,
			width: info.width,
			height: info.height,
			mediatype: info.mediatype
		};
		ve.init.platform.imageInfoCache.set( obj );

		this.checkChanged();
		this.switchPanels( 'edit' );

		ve.track( 'activity.' + this.constructor.static.name, {
			action: 'search-confirm-image'
		} );
	}
};

/**
 * Update the filename fieldset (link to media page)
 */
ve.ui.MWMediaDialog.prototype.updateFilenameFieldset = function () {
	const title = mw.Title.newFromText( mw.libs.ve.normalizeParsoidResourceName( this.imageModel.getResourceName() ) );
	this.filenameFieldset.setLabel(
		$( '<span>' ).append(
			$( document.createTextNode( this.imageModel.getFilename() + ' ' ) ),
			$( '<a>' )
				.addClass( 've-ui-mwMediaDialog-description-link' )
				.attr( 'href', title.getUrl() )
				.attr( 'target', '_blank' )
				.attr( 'rel', 'noopener' )
				.text( ve.msg( 'visualeditor-dialog-media-content-description-link' ) )
		)
	);
};

/**
 * Handle image model alignment change
 *
 * @param {string} alignment Image alignment
 */
ve.ui.MWMediaDialog.prototype.onImageModelAlignmentChange = function ( alignment ) {
	alignment = alignment || 'none';

	// Select the item without triggering the 'choose' event
	this.positionSelect.selectItemByData( alignment !== 'none' ? alignment : undefined );

	this.positionCheckbox.setSelected( alignment !== 'none' );
	this.checkChanged();
};

/**
 * Handle image model type change
 *
 * @param {string} type Image type
 */
ve.ui.MWMediaDialog.prototype.onImageModelTypeChange = function ( type ) {
	this.typeSelect.selectItemByData( type );

	this.borderCheckbox.setDisabled(
		!this.imageModel.isBorderable()
	);

	this.borderCheckbox.setSelected(
		this.imageModel.isBorderable() && this.imageModel.hasBorder()
	);
	this.checkChanged();
};

/**
 * Handle change event on the positionCheckbox element.
 *
 * @param {boolean} isSelected Checkbox status
 */
ve.ui.MWMediaDialog.prototype.onPositionCheckboxChange = function ( isSelected ) {
	const currentModelAlignment = this.imageModel.getAlignment();

	this.positionSelect.setDisabled( !isSelected );
	this.checkChanged();
	// Only update the model if the current value is different than that
	// of the image model
	if (
		( currentModelAlignment === 'none' && isSelected ) ||
		( currentModelAlignment !== 'none' && !isSelected )
	) {
		if ( isSelected ) {
			// Picking a floating alignment value will create a block image
			// no matter what the type is, so in here we want to calculate
			// the default alignment of a block to set as our initial alignment
			// in case the checkbox is clicked but there was no alignment set
			// previously.
			const newPositionValue = this.imageModel.getDefaultDir( 'mwBlockImage' );
			this.imageModel.setAlignment( newPositionValue );
		} else {
			// If we're unchecking the box, always set alignment to none and unselect the position widget
			this.imageModel.setAlignment( 'none' );
		}
	}
};

/**
 * Handle change event on the positionCheckbox element.
 *
 * @param {boolean} isSelected Checkbox status
 */
ve.ui.MWMediaDialog.prototype.onBorderCheckboxChange = function ( isSelected ) {
	// Only update if the value is different than the model
	if ( this.imageModel.hasBorder() !== isSelected ) {
		// Update the image model
		this.imageModel.toggleBorder( isSelected );
		this.checkChanged();
	}
};

/**
 * Handle change event on the positionSelect element.
 *
 * @param {OO.ui.ButtonOptionWidget} item Selected item
 */
ve.ui.MWMediaDialog.prototype.onPositionSelectChoose = function ( item ) {
	const position = item.getData();

	// Only update if the value is different than the model
	if ( this.imageModel.getAlignment() !== position ) {
		this.imageModel.setAlignment( position );
		this.checkChanged();
	}
};

/**
 * Handle change event on the typeSelect element.
 *
 * @param {OO.ui.MenuOptionWidget} item Selected item
 */
ve.ui.MWMediaDialog.prototype.onTypeSelectChoose = function ( item ) {
	const type = item.getData();

	// Only update if the value is different than the model
	if ( this.imageModel.getType() !== type ) {
		this.imageModel.setType( type );
		this.checkChanged();
	}

	// If type is 'frame', custom size is ignored
	if ( type === 'frame' ) {
		this.sizeWidget.setSizeType( 'default' );
	}
};

/**
 * Handle changeSizeType events from the MediaSizeWidget
 *
 * @param {string} sizeType Size type
 */
ve.ui.MWMediaDialog.prototype.onChangeSizeType = function ( sizeType ) {
	// type=frame is not resizeable, so change it to type=thumb
	if ( sizeType === 'custom' && this.imageModel.getType() === 'frame' ) {
		this.imageModel.setType( 'thumb' );
	}

	this.checkChanged();
};

/**
 * Respond to change in alternate text
 *
 * @param {string} text New alternate text
 */
ve.ui.MWMediaDialog.prototype.onAlternateTextChange = function ( text ) {
	this.imageModel.setAltText( text );
	this.checkChanged();
};

/**
 * When changes occur, enable the apply button.
 */
ve.ui.MWMediaDialog.prototype.checkChanged = function () {
	let captionChanged = false;

	// Only check 'changed' status after the model has finished
	// building itself
	if ( !this.isSettingUpModel ) {
		captionChanged = !!this.captionTarget && this.captionTarget.hasBeenModified();

		if (
			this.imageModel &&
			// Activate or deactivate the apply/insert buttons
			// Make sure sizes are valid first
			this.sizeWidget.isValid() &&
			(
				// Check that the model or caption changed
				this.isInsertion ||
				captionChanged ||
				this.imageModel.hasBeenModified()
			)
		) {
			this.actions.setAbilities( { insert: true, done: true } );
		} else {
			this.actions.setAbilities( { insert: false, done: false } );
		}
	}
};

/**
 * @inheritdoc
 */
ve.ui.MWMediaDialog.prototype.getSetupProcess = function ( data ) {
	return ve.ui.MWMediaDialog.super.prototype.getSetupProcess.call( this, data )
		.next( () => {
			const isReadOnly = this.isReadOnly();

			// Set language for search results
			this.search.setLang( this.getFragment().getDocument().getLang() );

			if ( this.selectedNode ) {
				this.isInsertion = false;
				// Create image model
				this.imageModel = ve.dm.MWImageModel.static.newFromImageNode( this.selectedNode );
				this.attachImageModel();

				if ( !this.imageModel.isDefaultSize() ) {
					// To avoid dirty diff in case where only the image changes,
					// we will store the initial bounding box, in case the image
					// is not defaultSize
					this.imageModel.setBoundingBox( this.imageModel.getCurrentDimensions() );
				}
				// Store initial hash to compare against
				this.imageModel.storeInitialHash( this.imageModel.getHashObject() );
			} else {
				this.isInsertion = true;
			}

			this.search.setup();
			// Try to populate with user uploads
			this.search.queryMediaQueue();
			this.resetCaption();

			this.altTextInput.setReadOnly( isReadOnly );
			this.positionCheckbox.setDisabled( isReadOnly );
			// TODO: This widget is not readable when disabled
			this.positionSelect.setDisabled( isReadOnly );
			this.typeSelectDropdown.setDisabled( isReadOnly );
			this.borderCheckbox.setDisabled( isReadOnly );
			this.sizeWidget.setDisabled( isReadOnly );

			// Pass `true` to avoid focussing. If we focus the image caption widget during dialog
			// opening, and it wants to display a context menu, it will be mispositioned.
			this.switchPanels( this.selectedNode ? 'edit' : 'search', true );

			this.actions.setAbilities( { upload: false, save: false, insert: false, done: false } );

			this.mediaUploadBookletInit = false;
			if ( data.file && this.mediaUploadBooklet ) {
				this.searchTabs.setTabPanel( 'upload' );
				this.mediaUploadBooklet.setFile( data.file );
			}
		} );
};

/**
 * Switch between the edit and insert/search panels
 *
 * @param {string} panel Panel name
 * @param {boolean} [noFocus=false] Do not put focus into the default field of the panel
 */
ve.ui.MWMediaDialog.prototype.switchPanels = function ( panel, noFocus ) {
	switch ( panel ) {
		case 'edit':
			this.setSize( this.constructor.static.size );
			// Set the edit panel
			this.panels.setItem( this.mediaSettingsLayout );
			// Focus the general settings page
			this.mediaSettingsLayout.setTabPanel( 'general' );
			// Parent functionality (edit/insert/readonly)
			this.actions.setMode( this.getMode() );
			if ( !noFocus ) {
				// Focus the caption surface
				this.captionTarget.focus();
			}
			// Auto-sized alt text field is populated while hidden,
			// so force a manual resize now.
			this.altTextInput.adjustSize( true );
			break;
		case 'search':
			this.setSize( 'larger' );
			this.selectedImageInfo = null;
			// Set the edit panel
			this.panels.setItem( this.mediaSearchPanel );
			this.searchTabs.setTabPanel( 'search' );
			this.searchTabs.toggleMenu( true );
			this.actions.setMode( this.imageModel ? 'change' : 'select' );
			if ( !noFocus ) {
				this.search.getQuery().focus().select();
			}
			// Layout pending items
			this.search.runLayoutQueue();
			break;
		default:
		case 'imageInfo':
			this.setSize( 'larger' );
			// Hide/show buttons
			this.actions.setMode( 'info' );
			// Hide/show the panels
			this.panels.setItem( this.mediaImageInfoPanel );
			break;
	}
	this.currentPanel = panel || 'imageinfo';
};

/**
 * Attach the image model to the dialog
 */
ve.ui.MWMediaDialog.prototype.attachImageModel = function () {
	if ( this.imageModel ) {
		this.imageModel.disconnect( this );
		this.sizeWidget.disconnect( this );
	}

	// Events
	this.imageModel.connect( this, {
		alignmentChange: 'onImageModelAlignmentChange',
		typeChange: 'onImageModelTypeChange',
		sizeDefaultChange: 'checkChanged'
	} );

	// Set up
	// Ignore the following changes in validation while we are
	// setting up the initial tools according to the model state
	this.isSettingUpModel = true;

	// Filename
	this.updateFilenameFieldset();

	// Size widget
	this.sizeWidget.setScalable( this.imageModel.getScalable() );
	this.sizeWidget.connect( this, {
		changeSizeType: 'onChangeSizeType',
		change: 'checkChanged',
		valid: 'checkChanged'
	} );

	// Initialize size
	this.sizeWidget.setSizeType( this.imageModel.isDefaultSize() ? 'default' : 'custom' );

	// Update default dimensions
	this.sizeWidget.updateDefaultDimensions();

	// Set initial alt text
	this.altTextInput.setValue( this.imageModel.getAltText() );

	// Set initial alignment
	this.positionSelect.setDisabled( !this.imageModel.isAligned() );
	this.positionSelect.selectItemByData( this.imageModel.isAligned() && this.imageModel.getAlignment() );
	this.positionCheckbox.setSelected( this.imageModel.isAligned() );

	// Border flag
	this.borderCheckbox.setDisabled( !this.imageModel.isBorderable() );
	this.borderCheckbox.setSelected( this.imageModel.isBorderable() && this.imageModel.hasBorder() );

	// Type select
	this.typeSelect.selectItemByData( this.imageModel.getType() || 'none' );

	this.isSettingUpModel = false;
};

/**
 * Reset the caption surface
 */
ve.ui.MWMediaDialog.prototype.resetCaption = function () {
	const doc = this.getFragment().getDocument();

	// Get existing caption. We only do this in setup, because the caption
	// should not reset to original if the image is replaced or edited.
	//
	// If the selected node is a block image and the caption already exists,
	// store the initial caption and set it as the caption document
	if (
		this.imageModel &&
		this.selectedNode &&
		this.selectedNode.getDocument() &&
		this.selectedNode instanceof ve.dm.MWBlockImageNode
	) {
		const captionNode = this.selectedNode.getCaptionNode();
		if ( captionNode && captionNode.getLength() > 0 ) {
			this.imageModel.setCaptionDocument(
				this.selectedNode.getDocument().cloneFromRange( captionNode.getRange() )
			);
		}
	}

	let captionDocument;
	if ( this.imageModel ) {
		captionDocument = this.imageModel.getCaptionDocument();
	} else {
		captionDocument = doc.cloneWithData( [
			{ type: 'paragraph', internal: { generated: 'wrapper' } },
			{ type: '/paragraph' },
			{ type: 'internalList' },
			{ type: '/internalList' }
		] );
	}

	// Set document
	this.captionTarget.setDocument( captionDocument );
	this.captionTarget.setReadOnly( this.isReadOnly() );
};

/**
 * @inheritdoc
 */
ve.ui.MWMediaDialog.prototype.getReadyProcess = function ( data ) {
	return ve.ui.MWMediaDialog.super.prototype.getReadyProcess.call( this, data )
		.next( () => {
			if ( !data.file ) {
				this.switchPanels( this.selectedNode ? 'edit' : 'search' );
			}
			// Revalidate size
			this.sizeWidget.validateDimensions();
		} );
};

/**
 * @inheritdoc
 */
ve.ui.MWMediaDialog.prototype.getTeardownProcess = function ( data ) {
	return ve.ui.MWMediaDialog.super.prototype.getTeardownProcess.call( this, data )
		.first( () => {
			this.mediaSettingsLayout.resetScroll();
			// Cleanup
			this.search.getQuery().setValue( '' );
			this.search.teardown();
			if ( this.imageModel ) {
				this.imageModel.disconnect( this );
				this.sizeWidget.disconnect( this );
			}
			this.captionTarget.clear();
			this.imageModel = null;
		} );
};

/**
 * @inheritdoc
 */
ve.ui.MWMediaDialog.prototype.getActionProcess = function ( action ) {
	let handler;

	switch ( action ) {
		case 'change':
			handler = function () {
				this.switchPanels( 'search' );
			};

			ve.track( 'activity.' + this.constructor.static.name, {
				action: 'search-change-image'
			} );
			break;
		case 'back':
			handler = function () {
				this.switchPanels( 'edit' );
			};
			break;
		case 'choose':
			handler = function () {
				this.confirmSelectedImage();
				this.switchPanels( 'edit' );
			};
			break;
		case 'cancelchoose':
			handler = function () {
				this.switchPanels( 'search' );
			};
			ve.track( 'activity.' + this.constructor.static.name, {
				action: 'search-change-image'
			} );
			break;
		case 'cancelupload':
			handler = function () {
				this.searchTabs.setTabPanel( 'upload' );
				this.searchTabs.toggleMenu( true );
				return this.mediaUploadBooklet.initialize();
			};
			break;
		case 'upload':
			ve.track( 'activity.' + this.constructor.static.name, {
				action: 'search-upload-image'
			} );
			return new OO.ui.Process( this.mediaUploadBooklet.uploadFile() );
		case 'save':
			return new OO.ui.Process( this.mediaUploadBooklet.saveFile() );
		case 'done':
		case 'insert':
			handler = function () {
				const surfaceModel = this.getFragment().getSurface();

				// Update from the form
				this.imageModel.setAltText( this.altTextInput.getValue() );
				this.imageModel.setCaptionDocument(
					this.captionTarget.getSurface().getModel().getDocument()
				);

				if (
					// There was an initial node
					this.selectedNode &&
					// And we didn't change the image type block/inline or vice versa
					this.selectedNode.type === this.imageModel.getImageNodeType() &&
					// And we didn't change the image itself
					this.selectedNode.getAttribute( 'src' ) ===
						this.imageModel.getImageSource()
				) {
					// We only need to update the attributes of the current node
					this.imageModel.updateImageNode( this.selectedNode, surfaceModel );
				} else {
					// Replacing an image or inserting a brand new one
					this.fragment = this.imageModel.insertImageNode( this.getFragment() );
				}

				this.close( { action: action } );
			};
			break;
		default:
			return ve.ui.MWMediaDialog.super.prototype.getActionProcess.call( this, action );
	}

	return new OO.ui.Process( handler, this );
};

/* Registration */

ve.ui.windowFactory.register( ve.ui.MWMediaDialog );
