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

/**
 * DataModel MediaWiki image node.
 *
 * @class
 * @abstract
 * @extends ve.dm.GeneratedContentNode
 * @mixes ve.dm.FocusableNode
 * @mixes ve.dm.ResizableNode
 *
 * @constructor
 */
ve.dm.MWImageNode = function VeDmMWImageNode() {
	// Parent constructor
	ve.dm.GeneratedContentNode.call( this );

	// Mixin constructors
	ve.dm.FocusableNode.call( this );
	// ve.dm.MWResizableNode doesn't exist
	ve.dm.ResizableNode.call( this );

	this.scalablePromise = null;

	// Use 'bitmap' as default media type until we can
	// fetch the actual media type from the API
	this.mediaType = 'BITMAP';

	// Initialize
	this.constructor.static.syncScalableToType(
		this.getAttribute( 'type' ),
		this.mediaType,
		this.getScalable()
	);

	// Events
	this.connect( this, { attributeChange: 'onAttributeChange' } );
};

/* Inheritance */

OO.inheritClass( ve.dm.MWImageNode, ve.dm.GeneratedContentNode );

OO.mixinClass( ve.dm.MWImageNode, ve.dm.FocusableNode );

OO.mixinClass( ve.dm.MWImageNode, ve.dm.ResizableNode );

/* Static methods */

ve.dm.MWImageNode.static.rdfaToTypes = ( function () {
	const rdfaToType = {};

	[ 'File', 'Image', 'Video', 'Audio' ].forEach( ( mediaClass ) => {
		rdfaToType[ 'mw:' + mediaClass ] = { mediaClass: mediaClass, frameType: 'none' };
		rdfaToType[ 'mw:' + mediaClass + '/Frameless' ] = { mediaClass: mediaClass, frameType: 'frameless' };
		// Block image only:
		rdfaToType[ 'mw:' + mediaClass + '/Thumb' ] = { mediaClass: mediaClass, frameType: 'thumb' };
		rdfaToType[ 'mw:' + mediaClass + '/Frame' ] = { mediaClass: mediaClass, frameType: 'frame' };
	} );

	return rdfaToType;
}() );

/**
 * Get RDFa type
 *
 * @static
 * @param {string} mediaClass Media class, one of 'File', 'Image', 'Video' or 'Audio'
 * @param {string} frameType Frame type, one of 'none', 'frameless', 'thumb' or 'frame'
 * @param {boolean} isError Whether the included media file is missing
 * @return {string} RDFa type
 */
ve.dm.MWImageNode.static.getRdfa = function ( mediaClass, frameType, isError ) {
	return ( isError ? 'mw:Error ' : '' ) + 'mw:' + mediaClass + {
		none: '',
		frameless: '/Frameless',
		// Block image only:
		thumb: '/Thumb',
		frame: '/Frame'
	}[ frameType ];
};

/**
 * Map media tags to source attributes
 *
 * @type {Object}
 */
ve.dm.MWImageNode.static.tagsToSrcAttrs = {
	img: 'src',
	audio: null,
	video: 'poster',
	span: null
};

/**
 * @inheritdoc ve.dm.GeneratedContentNode
 */
ve.dm.MWImageNode.static.getHashObjectForRendering = function ( dataElement ) {
	// "Rendering" is just the URL of the thumbnail, so we only
	// care about src & dimensions
	return {
		type: 'mwImage',
		resource: dataElement.attributes.resource,
		width: dataElement.attributes.width,
		height: dataElement.attributes.height
	};
};

ve.dm.MWImageNode.static.getMatchRdfaTypes = function () {
	return Object.keys( this.rdfaToTypes );
};

ve.dm.MWImageNode.static.allowedRdfaTypes = [ 'mw:Error' ];

ve.dm.MWImageNode.static.isDiffComparable = function ( element, other ) {
	// Images with different src's shouldn't be diffed
	return element.type === other.type && element.attributes.resource === other.attributes.resource;
};

ve.dm.MWImageNode.static.describeChanges = function ( attributeChanges, attributes ) {
	const customKeys = [ 'width', 'height', 'defaultSize', 'src', 'href' ],
		descriptions = [];

	function describeSize( width, height ) {
		return width + ve.msg( 'visualeditor-dimensionswidget-times' ) + height + ve.msg( 'visualeditor-dimensionswidget-px' );
	}

	if ( 'width' in attributeChanges || 'height' in attributeChanges ) {
		let sizeFrom, sizeTo;
		if ( attributeChanges.defaultSize && attributeChanges.defaultSize.from === true ) {
			sizeFrom = ve.msg( 'visualeditor-mediasizewidget-sizeoptions-default' );
		} else {
			sizeFrom = describeSize(
				'width' in attributeChanges ? attributeChanges.width.from : attributes.width,
				'height' in attributeChanges ? attributeChanges.height.from : attributes.height
			);
		}
		if ( attributeChanges.defaultSize && attributeChanges.defaultSize.to === true ) {
			sizeTo = ve.msg( 'visualeditor-mediasizewidget-sizeoptions-default' );
		} else {
			sizeTo = describeSize(
				'width' in attributeChanges ? attributeChanges.width.to : attributes.width,
				'height' in attributeChanges ? attributeChanges.height.to : attributes.height
			);
		}

		descriptions.push(
			ve.htmlMsg( 'visualeditor-changedesc-image-size', this.wrapText( 'del', sizeFrom ), this.wrapText( 'ins', sizeTo ) )
		);
	}
	for ( const key in attributeChanges ) {
		if ( customKeys.indexOf( key ) === -1 ) {
			if ( key === 'borderImage' && !attributeChanges.borderImage.from && !attributeChanges.borderImage.to ) {
				// Skip noise from the data model
				continue;
			}
			const change = this.describeChange( key, attributeChanges[ key ] );
			if ( change ) {
				descriptions.push( change );
			}
		}
	}
	return descriptions;
};

ve.dm.MWImageNode.static.describeChange = function ( key, change ) {
	switch ( key ) {
		case 'align':
			return ve.htmlMsg( 'visualeditor-changedesc-align',
				// The following messages are used here:
				// * visualeditor-align-desc-left
				// * visualeditor-align-desc-right
				// * visualeditor-align-desc-center
				// * visualeditor-align-desc-default
				// * visualeditor-align-desc-none
				this.wrapText( 'del', ve.msg( 'visualeditor-align-desc-' + change.from ) ),
				this.wrapText( 'ins', ve.msg( 'visualeditor-align-desc-' + change.to ) )
			);
		case 'originalWidth':
		case 'originalHeight':
		case 'originalClasses':
		case 'unrecognizedClasses':
			return;
		// TODO: Handle valign
	}
	// Parent method
	return ve.dm.Node.static.describeChange.apply( this, arguments );
};

/**
 * Take the given dimensions and scale them to thumbnail size.
 *
 * @param {Object} dimensions Width and height of the image
 * @param {string} [mediaType] Media type 'DRAWING' or 'BITMAP'
 * @return {Object} The new width and height of the scaled image
 */
ve.dm.MWImageNode.static.scaleToThumbnailSize = function ( dimensions, mediaType ) {
	const defaultThumbSize = mw.config.get( 'wgVisualEditorConfig' )
		.thumbLimits[ mw.user.options.get( 'thumbsize' ) ];

	mediaType = mediaType || 'BITMAP';

	if ( dimensions.width && dimensions.height ) {
		// Use dimensions
		// Resize to default thumbnail size, but only if the image itself
		// isn't smaller than the default size
		// For svg/drawings, the default wiki size is always applied
		if ( dimensions.width > defaultThumbSize || mediaType === 'DRAWING' ) {
			return ve.dm.Scalable.static.getDimensionsFromValue( {
				width: defaultThumbSize
			}, dimensions.width / dimensions.height );
		}
	}
	return dimensions;
};

/**
 * Translate the image dimensions into new ones according to the bounding box.
 *
 * @param {Object} imageDimensions Width and height of the image
 * @param {Object} boundingBox The limit of the bounding box
 * @return {Object} The new width and height of the scaled image.
 */
ve.dm.MWImageNode.static.resizeToBoundingBox = function ( imageDimensions, boundingBox ) {
	const scale = Math.min(
		boundingBox.height / imageDimensions.height,
		boundingBox.width / imageDimensions.width
	);

	let newDimensions = ve.copy( imageDimensions );
	if ( scale < 1 ) {
		// Scale down
		newDimensions = {
			width: Math.floor( newDimensions.width * scale ),
			height: Math.floor( newDimensions.height * scale )
		};
	}
	return newDimensions;
};

/**
 * Update image scalable properties according to the image type.
 *
 * @param {string} type The new image type
 * @param {string} mediaType Image media type 'DRAWING' or 'BITMAP'
 * @param {ve.dm.Scalable} scalable The scalable object to update
 */
ve.dm.MWImageNode.static.syncScalableToType = function ( type, mediaType, scalable ) {
	const defaultThumbSize = mw.config.get( 'wgVisualEditorConfig' )
		.thumbLimits[ mw.user.options.get( 'thumbsize' ) ];

	const originalDimensions = scalable.getOriginalDimensions();

	// We can only set default dimensions if we have the original ones
	if ( originalDimensions ) {
		if ( type === 'thumb' || type === 'frameless' ) {
			// Set the default size to that in the wiki configuration if
			// 1. The original image width is not smaller than the default
			// 2. If the image is an SVG drawing
			let dimensions;
			if ( originalDimensions.width >= defaultThumbSize || mediaType === 'DRAWING' ) {
				dimensions = ve.dm.Scalable.static.getDimensionsFromValue( {
					width: defaultThumbSize
				}, scalable.getRatio() );
			} else {
				dimensions = ve.dm.Scalable.static.getDimensionsFromValue(
					originalDimensions,
					scalable.getRatio()
				);
			}
			scalable.setDefaultDimensions( dimensions );
		} else {
			scalable.setDefaultDimensions( originalDimensions );
		}
	}

	// Deal with maximum dimensions for images and drawings
	if ( mediaType === 'DRAWING' ) {
		// Vector images are scalable past their original dimensions
		// EnforcedMax may have previously been set to true
		scalable.setEnforcedMax( false );

	} else if ( mediaType === 'AUDIO' ) {
		// Audio files are scalable to any width but have fixed height
		scalable.fixedRatio = false;
		scalable.setMinDimensions( { width: 1, height: 32 } );
		// TODO: No way to enforce max height but not max width
		scalable.setMaxDimensions( { width: 99999, height: 32 } );
		scalable.setEnforcedMax( true );
		scalable.setEnforcedMin( true );

		// Default dimensions for audio files are 0x0, which is no good
		scalable.setDefaultDimensions( { width: defaultThumbSize, height: 32 } );

	} else {
		// Raster image files are limited to their original dimensions
		if ( originalDimensions ) {
			scalable.setMaxDimensions( originalDimensions );
			scalable.setEnforcedMax( true );
		} else {
			scalable.setEnforcedMax( false );
		}
	}
	// TODO: Some day, when $wgSvgMaxSize works properly in MediaWiki
	// we can add it back as max dimension consideration.
};

/**
 * Get the scalable promise which fetches original dimensions from the API
 *
 * @param {string} filename The image filename whose details the scalable will represent
 * @return {jQuery.Promise} Promise which resolves after the image size details are fetched from the API
 */
ve.dm.MWImageNode.static.getScalablePromise = function ( filename ) {
	// On the first call set off an async call to update the scalable's
	// original dimensions from the API.
	if ( ve.init.platform.imageInfoCache ) {
		return ve.init.platform.imageInfoCache.get( filename ).then( ( info ) => {
			if ( !info || info.missing ) {
				return ve.createDeferred().reject().promise();
			}
			return info;
		} );
	} else {
		return ve.createDeferred().reject().promise();
	}
};

/* Methods */

/**
 * Respond to attribute change.
 * Update the rendering of the 'align', src', 'width' and 'height' attributes
 * when they change in the model.
 *
 * @param {string} key Attribute key
 * @param {string} from Old value
 * @param {string} to New value
 */
ve.dm.MWImageNode.prototype.onAttributeChange = function ( key, from, to ) {
	if ( key === 'type' ) {
		this.constructor.static.syncScalableToType( to, this.mediaType, this.getScalable() );
	}
};

/**
 * Get the normalised filename of the image
 *
 * @return {string} Filename (including namespace)
 */
ve.dm.MWImageNode.prototype.getFilename = function () {
	return mw.libs.ve.normalizeParsoidResourceName( this.getAttribute( 'resource' ) || '' );
};

/**
 * @inheritdoc
 */
ve.dm.MWImageNode.prototype.getScalable = function () {
	if ( !this.scalablePromise ) {
		this.scalablePromise = ve.dm.MWImageNode.static.getScalablePromise( this.getFilename() );
		// If the promise was already resolved before getScalablePromise returned, then jQuery will execute the done straight away.
		// So don't just do getScalablePromise( ... ).done because we need to make sure that this.scalablePromise gets set first.
		this.scalablePromise.done( ( info ) => {
			if ( info ) {
				this.getScalable().setOriginalDimensions( {
					width: info.width,
					height: info.height
				} );
				const oldMediaType = this.mediaType;
				// Update media type
				this.mediaType = info.mediatype;
				// Update according to type
				this.constructor.static.syncScalableToType(
					this.getAttribute( 'type' ),
					this.mediaType,
					this.getScalable()
				);
				this.emit( 'attributeChange', 'mediaType', oldMediaType, this.mediaType );
			}
		} );
	}
	// Mixin method
	return ve.dm.ResizableNode.prototype.getScalable.call( this );
};

/**
 * @inheritdoc
 */
ve.dm.MWImageNode.prototype.createScalable = function () {
	return new ve.dm.Scalable( {
		currentDimensions: {
			width: this.getAttribute( 'width' ),
			height: this.getAttribute( 'height' )
		},
		minDimensions: {
			width: 1,
			height: 1
		}
	} );
};

/**
 * Get symbolic name of media type.
 *
 * Example values: "BITMAP" for JPEG or PNG images; "DRAWING" for SVG graphics
 *
 * @return {string|undefined} Symbolic media type name, or undefined if empty
 */
ve.dm.MWImageNode.prototype.getMediaType = function () {
	return this.mediaType;
};

/**
 * Get RDFa type
 *
 * @return {string} RDFa type
 */
ve.dm.MWImageNode.prototype.getRdfa = function () {
	return this.constructor.static.getRdfa(
		this.getAttribute( 'mediaClass' ),
		this.getAttribute( 'type' ),
		this.getAttribute( 'isError' )
	);
};