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

/**
 * DataModel MediaWiki extension node.
 *
 * @class
 * @abstract
 * @extends ve.dm.LeafNode
 * @mixes ve.dm.FocusableNode
 * @mixes ve.dm.GeneratedContentNode
 *
 * @constructor
 */
ve.dm.MWExtensionNode = function VeDmMWExtensionNode() {
	// Parent constructor
	ve.dm.MWExtensionNode.super.apply( this, arguments );

	// Mixin constructors
	ve.dm.GeneratedContentNode.call( this );
	ve.dm.FocusableNode.call( this );
};

/* Inheritance */

OO.inheritClass( ve.dm.MWExtensionNode, ve.dm.LeafNode );
OO.mixinClass( ve.dm.MWExtensionNode, ve.dm.FocusableNode );
OO.mixinClass( ve.dm.MWExtensionNode, ve.dm.GeneratedContentNode );

/* Static members */

ve.dm.MWExtensionNode.static.enableAboutGrouping = true;

ve.dm.MWExtensionNode.static.matchTagNames = null;

ve.dm.MWExtensionNode.static.childNodeTypes = [];

/**
 * HTML tag name.
 *
 * @static
 * @property {string}
 * @inheritable
 */
ve.dm.MWExtensionNode.static.tagName = null;

/**
 * Name of the MediaWiki parser extension tag. (Not related to the name of the MediaWiki extension.)
 *
 * @static
 * @property {string}
 * @inheritable
 */
ve.dm.MWExtensionNode.static.extensionName = null;

ve.dm.MWExtensionNode.static.getMatchRdfaTypes = function () {
	return [ 'mw:Extension/' + this.extensionName ];
};

/**
 * @inheritdoc
 * @param {Node[]} domElements
 * @param {ve.dm.Converter} converter
 * @param {string} [type] Type to give dataElement, defaults to static.name
 */
ve.dm.MWExtensionNode.static.toDataElement = function ( domElements, converter, type ) {
	const mwDataJSON = domElements[ 0 ].getAttribute( 'data-mw' ),
		mwData = mwDataJSON ? JSON.parse( mwDataJSON ) : {};

	const dataElement = {
		type: type || this.name,
		attributes: {
			mw: mwData,
			originalMw: mwDataJSON
		}
	};

	this.storeGeneratedContents( dataElement, domElements, converter.getStore() );
	// Sub-classes should not modify dataElement beyond this point as it will invalidate the cache

	return dataElement;
};

/**
 * @inheritdoc ve.dm.Node
 */
ve.dm.MWExtensionNode.static.cloneElement = function () {
	// Parent method
	const clone = ve.dm.MWExtensionNode.super.static.cloneElement.apply( this, arguments );
	delete clone.attributes.originalMw;
	return clone;
};

ve.dm.MWExtensionNode.static.toDomElements = function ( dataElement, doc, converter ) {
	const originalMw = dataElement.attributes.originalMw;

	let els;
	// If the transclusion is unchanged just send back the
	// original DOM elements so selser can skip over it
	if (
		dataElement.originalDomElementsHash &&
		originalMw && ve.compare( dataElement.attributes.mw, JSON.parse( originalMw ) )
	) {
		// originalDomElements is also used for CE rendering so return a copy
		els = ve.copyDomElements( converter.getStore().value( dataElement.originalDomElementsHash ), doc );
	} else {
		const store = converter.getStore();
		let value;
		if (
			converter.doesModeNeedRendering() &&
			// Use getHashObjectForRendering to get the rendering from the store
			( value = store.value( store.hashOfValue( null, OO.getHash( [ this.getHashObjectForRendering( dataElement ), undefined ] ) ) ) )
		) {
			// For the clipboard use the current DOM contents so the user has something
			// meaningful to paste into external applications
			els = ve.copyDomElements( value, doc );
		} else {
			const el = doc.createElement( this.tagName );
			el.setAttribute( 'typeof', 'mw:Extension/' + this.getExtensionName( dataElement ) );
			el.setAttribute( 'data-mw', JSON.stringify( dataElement.attributes.mw ) );
			els = [ el ];
		}
	}
	return els;
};

ve.dm.MWExtensionNode.static.getHashObject = function ( dataElement ) {
	return {
		type: dataElement.type,
		mw: ve.copy( dataElement.attributes.mw )
	};
};

/**
 * Get name of the MediaWiki parser extension tag.
 *
 * Static version for toDomElements
 *
 * @static
 * @param {Object} dataElement Data element
 * @return {string} Extension name
 */
ve.dm.MWExtensionNode.static.getExtensionName = function () {
	return this.extensionName;
};

ve.dm.MWExtensionNode.static.describeChanges = function ( attributeChanges, attributes, element ) {
	const descriptions = [],
		fromBody = attributeChanges.mw.from.body,
		toBody = attributeChanges.mw.to.body;

	if ( attributeChanges.mw ) {
		// HACK: Try to generate an '<Extension> has changed' message using the associated tool's title
		const tools = ve.ui.toolFactory.getRelatedItems( [ ve.dm.nodeFactory.createFromElement( element ) ] );
		if ( tools.length ) {
			descriptions.push( ve.msg( 'visualeditor-changedesc-unknown',
				OO.ui.resolveMsg( ve.ui.toolFactory.lookup( tools[ 0 ].name ).static.title )
			) );
		}
		// Compare body - default behaviour in #describeChange does nothing
		if ( !ve.compare( fromBody, toBody ) ) {
			const change = this.describeChange( 'body', {
				from: fromBody && fromBody.extsrc,
				to: toBody && toBody.extsrc
			} );
			if ( change ) {
				descriptions.push( change );
			}
		}
		// Append attribute changes
		// Parent method
		Array.prototype.push.apply( descriptions, ve.dm.MWExtensionNode.super.static.describeChanges.call(
			this,
			ve.ui.DiffElement.static.compareAttributes( attributeChanges.mw.from.attrs || {}, attributeChanges.mw.to.attrs || {} ),
			attributes
		) );
		return descriptions;
	}
	// 'mw' should be the only attribute that changes...
	return [];
};

ve.dm.MWExtensionNode.static.describeChange = function ( key, change ) {
	if ( key === 'body' ) {
		if ( change.from && change.to ) {
			const store = new ve.dm.HashValueStore();
			const linearDiffer = new ve.DiffMatchPatch( store, store );
			const trimNewlines = /^\n+|\n+$/g;
			const linearDiff = linearDiffer.getCleanDiff(
				change.from.replace( trimNewlines, '' ).split( '' ),
				change.to.replace( trimNewlines, '' ).split( '' ),
				{ keepOldText: false }
			);
			const div = document.createElement( 'div' );
			linearDiff.forEach( ( diffSection, i ) => {
				const [ type, data ] = diffSection;
				const text = data.join( '' );
				let el, nodeText;
				switch ( type ) {
					case ve.DiffMatchPatch.static.DIFF_DELETE:
						el = document.createElement( 'del' );
						nodeText = text;
						break;
					case ve.DiffMatchPatch.static.DIFF_INSERT:
						el = document.createElement( 'ins' );
						nodeText = text;
						break;
					case ve.DiffMatchPatch.static.DIFF_EQUAL: {
						el = document.createElement( 'span' );
						const lines = text.split( '\n' );
						const filteredLines = [];
						if ( lines.length === 1 ) {
							nodeText = text;
						} else {
							if ( i !== 0 ) {
								filteredLines.push( lines[ 0 ] );
							}
							if ( lines.length > 2 ) {
								filteredLines.push( '…' );
							}
							if ( i !== linearDiff.length - 1 ) {
								filteredLines.push( lines[ lines.length - 1 ] );
							}
							nodeText = filteredLines.join( '\n' );
						}
						break;
					}
				}
				el.appendChild( document.createTextNode( nodeText ) );
				div.appendChild( el );
			} );
			return [ div ];
		}
		return null;
	}
	// Parent method
	return ve.dm.MWExtensionNode.super.static.describeChange.apply( this, arguments );
};

/* Methods */

/**
 * Get name of the MediaWiki parser extension tag.
 *
 * @return {string} Extension name
 */
ve.dm.MWExtensionNode.prototype.getExtensionName = function () {
	return this.constructor.static.getExtensionName( this.element );
};