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

/**
 * Initialization Standalone platform.
 *
 *     @example
 *     var platform = new ve.init.sa.Platform( ve.messagePaths );
 *     platform.initialize().done( () => {
 *         $( document.body ).append( $( '<p>' ).text(
 *             platform.getMessage( 'visualeditor' )
 *         ) );
 *     } );
 *
 * @class
 * @extends ve.init.Platform
 *
 * @constructor
 * @param {string[]} [messagePaths] Message folder paths
 */
ve.init.sa.Platform = function VeInitSaPlatform( messagePaths ) {
	// Parent constructor
	ve.init.Platform.call( this );

	// Properties
	this.externalLinkUrlProtocolsRegExp = /^https?:\/\//i;
	this.unanchoredExternalLinkUrlProtocolsRegExp = /https?:\/\//i;
	this.messagePaths = messagePaths || [];
	this.parsedMessages = {};
	this.userLanguages = [ 'en' ];
};

/* Inheritance */

OO.inheritClass( ve.init.sa.Platform, ve.init.Platform );

/* Methods */

/** @inheritdoc */
ve.init.sa.Platform.prototype.getExternalLinkUrlProtocolsRegExp = function () {
	return this.externalLinkUrlProtocolsRegExp;
};

/** @inheritdoc */
ve.init.sa.Platform.prototype.getUnanchoredExternalLinkUrlProtocolsRegExp = function () {
	return this.unanchoredExternalLinkUrlProtocolsRegExp;
};

/** @inheritdoc */
ve.init.sa.Platform.prototype.notify = function ( message, title ) {
	const $notification = $( '<div>' ).addClass( 've-init-notification' );

	if ( title ) {
		$notification.append(
			// Never appends strings directly
			// eslint-disable-next-line no-jquery/no-append-html
			$( '<div>' ).addClass( 've-init-notification-title' ).append(
				typeof title === 'string' ? document.createTextNode( title ) : title
			)
		);
	}
	$notification.append(
		// Never appends strings directly
		// eslint-disable-next-line no-jquery/no-append-html
		$( '<div>' ).addClass( 've-init-notification-message' ).append(
			typeof message === 'string' ? document.createTextNode( message ) : message
		)
	);

	const $notificationWrapper = $( '<div>' ).addClass( 've-init-notification-wrapper' );
	$notificationWrapper.append( $notification );

	if ( !this.$notifications ) {
		this.$notifications = $( '<div>' ).addClass( 've-init-notifications' );
		$( document.body ).append( this.$notifications );
	}

	let closeId;

	function remove() {
		$notificationWrapper.remove();
	}
	function collapse() {
		$notificationWrapper.addClass( 've-init-notification-collapse' );
		$notificationWrapper.one( 'transitionend', remove );
	}
	function close() {
		clearTimeout( closeId );
		$notificationWrapper.removeClass( 've-init-notification-open' );
		$notificationWrapper.css( 'height', $notificationWrapper[ 0 ].clientHeight );
		$notificationWrapper.one( 'transitionend', collapse );
	}
	function open() {
		$notificationWrapper.addClass( 've-init-notification-open' );
		closeId = setTimeout( close, 5000 );
	}

	requestAnimationFrame( open );

	$notification.on( 'click', close );

	this.$notifications.append( $notificationWrapper );
};

/**
 * Get message folder paths
 *
 * @return {string[]} Message folder paths
 */
ve.init.sa.Platform.prototype.getMessagePaths = function () {
	return this.messagePaths;
};

/** @inheritdoc */
ve.init.sa.Platform.prototype.addMessages = function ( messages ) {
	$.i18n().load( messages, $.i18n().locale );
};

/**
 * @method
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.getMessage = $.i18n;

/**
 * @method
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.parseNumber = function ( value ) {
	// TODO: Support separated numbers such as (en)123,456.78 or (fr)123.456,78
	return parseFloat( value );
};

/**
 * @method
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.formatNumber = function ( number ) {
	return number.toLocaleString();
};

/**
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.getHtmlMessage = function ( key ) {
	const args = arguments,
		message = this.getMessage( key );
	let $message = $( [] ),
		lastOffset = 0;
	message.replace( /\$[0-9]+/g, ( placeholder, offset ) => {
		$message = $message.add( ve.sanitizeHtml( message.slice( lastOffset, offset ) ) );
		const placeholderIndex = +( placeholder.slice( 1 ) );
		const arg = args[ placeholderIndex ];
		$message = $message.add(
			typeof arg === 'string' ?
				// Arguments come from the code so shouldn't be sanitized
				document.createTextNode( arg ) :
				arg
		);
		lastOffset = offset + placeholder.length;
	} );
	$message = $message.add( ve.sanitizeHtml( message.slice( lastOffset ) ) );
	return $message.toArray();
};

/**
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.getConfig = function () {
	/* Standalone has no config yet */
	return null;
};

/**
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.getUserConfig = function ( keys ) {
	if ( Array.isArray( keys ) ) {
		const values = {};
		for ( let i = 0, l = keys.length; i < l; i++ ) {
			values[ keys[ i ] ] = this.getUserConfig( keys[ i ] );
		}
		return values;
	} else {
		try {
			return JSON.parse( localStorage.getItem( 've-' + keys ) );
		} catch ( e ) {
			return null;
		}
	}
};

/**
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.setUserConfig = function ( keyOrValueMap, value ) {
	if ( typeof keyOrValueMap === 'object' ) {
		for ( const i in keyOrValueMap ) {
			if ( Object.prototype.hasOwnProperty.call( keyOrValueMap, i ) ) {
				if ( !this.setUserConfig( i, keyOrValueMap[ i ] ) ) {
					// localStorage will fail if the quota is full, so further
					// sets won't work anyway.
					return false;
				}
			}
		}
	} else {
		try {
			localStorage.setItem( 've-' + keyOrValueMap, JSON.stringify( value ) );
		} catch ( e ) {
			return false;
		}
	}
	return true;
};

ve.init.sa.Platform.prototype.createSafeStorage = function ( storage ) {
	return new ve.init.sa.SafeStorage( storage );
};

/**
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.addParsedMessages = function ( messages ) {
	for ( const key in messages ) {
		this.parsedMessages[ key ] = messages[ key ];
	}
};

/**
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.getParsedMessage = function ( key ) {
	if ( Object.prototype.hasOwnProperty.call( this.parsedMessages, key ) ) {
		return this.parsedMessages[ key ];
	}
	// Fallback to regular messages, html escaping applied.
	return this.getMessage( key ).replace( /['"<>&]/g, ( char ) => ( {
		'\'': '&#039;',
		'"': '&quot;',
		'<': '&lt;',
		'>': '&gt;',
		'&': '&amp;'
	}[ char ] ) );
};

/**
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.getLanguageCodes = function () {
	return Object.keys( $.uls.data.getAutonyms() );
};

/**
 * @method
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.getLanguageName = $.uls.data.getAutonym;

/**
 * @method
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.getLanguageAutonym = $.uls.data.getAutonym;

/**
 * @method
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.getLanguageDirection = $.uls.data.getDir;

/**
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.getUserLanguages = function () {
	return this.userLanguages;
};

/**
 * @inheritdoc
 */
ve.init.sa.Platform.prototype.initialize = function () {
	const messagePaths = this.getMessagePaths(),
		locale = $.i18n().locale,
		languages = [ locale, 'en' ], // Always use 'en' as the final fallback
		languagesCovered = {},
		promises = [];
	let fallbacks = $.i18n.fallbacks[ locale ];

	if ( !VisualEditorSupportCheck() ) {
		return ve.createDeferred().reject().promise();
	}

	if ( !fallbacks ) {
		// Try to find something that has fallbacks (which means it's a language we know about)
		// by stripping things from the end. But collect all the intermediate ones in case we
		// go past languages that don't have fallbacks but do exist.
		const localeParts = locale.split( '-' );
		localeParts.pop();
		while ( localeParts.length && !fallbacks ) {
			const partialLocale = localeParts.join( '-' );
			languages.push( partialLocale );
			fallbacks = $.i18n.fallbacks[ partialLocale ];
			localeParts.pop();
		}
	}

	if ( fallbacks ) {
		languages.push( ...fallbacks );
	}

	this.userLanguages = languages;

	for ( let i = 0, iLen = languages.length; i < iLen; i++ ) {
		if ( languagesCovered[ languages[ i ] ] ) {
			continue;
		}
		languagesCovered[ languages[ i ] ] = true;

		// Lower-case the language code for the filename. jQuery.i18n does not case-fold
		// language codes, so we should not case-fold the second argument in #load.
		const filename = languages[ i ].toLowerCase() + '.json';

		for ( let j = 0, jLen = messagePaths.length; j < jLen; j++ ) {
			const deferred = ve.createDeferred();
			$.i18n().load( messagePaths[ j ] + filename, languages[ i ] )
				.always( deferred.resolve );
			promises.push( deferred.promise() );
		}
	}
	return ve.promiseAll( promises );
};