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

/**
 * Generic Initialization platform.
 *
 * @abstract
 * @mixes OO.EventEmitter
 *
 * @constructor
 */
ve.init.Platform = function VeInitPlatform() {
	// Mixin constructors
	OO.EventEmitter.call( this );

	this.localStorage = this.createLocalStorage();
	this.sessionStorage = this.createSessionStorage();

	// Register
	ve.init.platform = this;

	// Provide messages to OOUI
	OO.ui.getUserLanguages = this.getUserLanguages.bind( this );
	OO.ui.msg = this.getMessage.bind( this );

	// Notify those waiting for a platform that they can finish initialization
	setTimeout( () => {
		ve.init.Platform.static.deferredPlatform.resolve( ve.init.platform );
	} );
};

/* Inheritance */

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

/* Static Properties */

/**
 * A jQuery.Deferred that tracks when the platform has been created.
 *
 * @private
 */
ve.init.Platform.static.deferredPlatform = ve.createDeferred();

/**
 * A promise that tracks when ve.init.platform is ready for use.  When
 * this promise is resolved the platform will have been created and
 * initialized.
 *
 * This promise is safe to access early in VE startup before
 * `ve.init.platform` has been set.
 *
 * @property {jQuery.Promise}
 */
ve.init.Platform.static.initializedPromise = ve.init.Platform.static.deferredPlatform.promise().then( ( platform ) => platform.getInitializedPromise() );

/* Static Methods */

/**
 * Get client platform string from browser.
 *
 * @static
 * @inheritable
 * @return {string} Client platform string
 */
ve.init.Platform.static.getSystemPlatform = function () {
	return $.client.profile().platform;
};

/**
 * Check whether we are running in Edge.
 *
 * @static
 * @inheritable
 * @return {boolean} We are in Edge
 */
ve.init.Platform.static.isEdge = function () {
	return $.client.profile().name === 'edge';
};

/**
 * Check whether we are running on iOS
 *
 * @static
 * @inheritable
 * @return {boolean} We are running on iOS
 */
ve.init.Platform.static.isIos = function () {
	return /ipad|iphone|ipod/i.test( navigator.userAgent );
};

/* Methods */

/**
 * Get an anchored regular expression that matches allowed external link URLs
 * starting at the beginning of an input string.
 *
 * @abstract
 * @return {RegExp} Regular expression object
 */
ve.init.Platform.prototype.getExternalLinkUrlProtocolsRegExp = null;

/**
 * Get an unanchored regular expression that matches allowed external link URLs
 * anywhere in an input string.
 *
 * @abstract
 * @return {RegExp} Regular expression object
 */
ve.init.Platform.prototype.getUnanchoredExternalLinkUrlProtocolsRegExp = null;

/**
 * Get a regular expression that matches IDs used only for linking document
 * data to metadata. Use null if your document format does not have such IDs.
 *
 * @return {RegExp|null} Regular expression object
 */
ve.init.Platform.prototype.getMetadataIdRegExp = function () {
	return null;
};

/**
 * Show a read-only notification to the user.
 *
 * @abstract
 * @param {jQuery|string} message
 * @param {jQuery|string} [title]
 */
ve.init.Platform.prototype.notify = null;

/**
 * Get a platform config value
 *
 * @abstract
 * @param {string|string[]} key Config key, or list of keys
 * @return {any|Object} Config value, or keyed object of config values if list of keys provided
 */
ve.init.Platform.prototype.getConfig = null;

/**
 * Get a user config value
 *
 * @abstract
 * @param {string|string[]} key Config key, or list of keys
 * @return {any|Object} Config value, or keyed object of config values if list of keys provided
 */
ve.init.Platform.prototype.getUserConfig = null;

/**
 * Set a user config value
 *
 * @abstract
 * @param {string|Object} keyOrValueMap Key to set value for, or object mapping keys to values
 * @param {any} [value] Value to set (optional, only in use when key is a string)
 */
ve.init.Platform.prototype.setUserConfig = null;

/**
 * Create a safe storage object
 *
 * @abstract
 * @return {ve.init.SafeStorage}
 */
ve.init.Platform.prototype.createSafeStorage = null;

/**
 * Create a list storage object from a safe storage object
 *
 * @param {ve.init.SafeStorage} safeStorage
 * @return {ve.init.ConflictableStorage}
 */
ve.init.Platform.prototype.createConflictableStorage = function ( safeStorage ) {
	return ve.init.createConflictableStorage( safeStorage );
};

ve.init.Platform.prototype.createLocalStorage = function () {
	let localStorage;

	try {
		localStorage = window.localStorage;
	} catch ( e ) {}

	return this.createConflictableStorage( this.createSafeStorage( localStorage ) );
};

ve.init.Platform.prototype.createSessionStorage = function () {
	let sessionStorage;

	try {
		sessionStorage = window.sessionStorage;
	} catch ( e ) {}

	return this.createConflictableStorage( this.createSafeStorage( sessionStorage ) );
};

/**
 * Add multiple messages to the localization system.
 *
 * @abstract
 * @param {Object} messages Containing plain message values
 */
ve.init.Platform.prototype.addMessages = null;

/**
 * Get a message from the localization system.
 *
 * @abstract
 * @param {string} key Message key
 * @param {...any} [args] List of arguments which will be injected at $1, $2, etc. in the message
 * @return {string} Localized message, or key or '<' + key + '>' if message not found
 */
ve.init.Platform.prototype.getMessage = null;

/**
 * Get the current user's name, if the platform supports it
 *
 * @return {string|null} User name, or null if not applicable
 */
ve.init.Platform.prototype.getUserName = function () {
	return null;
};

/**
 * Parse a string into a number
 *
 * @abstract
 * @param {string} value String to be converted
 * @return {number} Number value, NaN if not a number
 */
ve.init.Platform.prototype.parseNumber = null;

/**
 * For a number as a string
 *
 * @abstract
 * @param {number} number Number to be formatted
 * @return {string} Formatted number
 */
ve.init.Platform.prototype.formatNumber = null;

/**
 * Get an HTML message from the localization system, with HTML or DOM arguments
 *
 * @abstract
 * @param {string} key Message key
 * @param {...any} [args] List of arguments which will be injected at $1, $2, etc. in the message
 * @return {Node[]} Localized message, or key or '<' + key + '>' if message not found
 */
ve.init.Platform.prototype.getHtmlMessage = null;

/**
 * Add multiple parsed messages to the localization system.
 *
 * @abstract
 * @param {Object} messages Map of message-key/html pairs
 */
ve.init.Platform.prototype.addParsedMessages = null;

/**
 * Get a parsed message as HTML string.
 *
 * Does not support $# replacements.
 *
 * @abstract
 * @param {string} key Message key
 * @return {string} Parsed localized message as HTML string
 */
ve.init.Platform.prototype.getParsedMessage = null;

/**
 * Get the user language and any fallback languages.
 *
 * @abstract
 * @return {string[]} User language strings
 */
ve.init.Platform.prototype.getUserLanguages = null;

/**
 * Get a list of URL entry points where media can be found.
 *
 * @abstract
 * @return {string[]} API URLs
 */
ve.init.Platform.prototype.getMediaSources = null;

/**
 * Get a list of all language codes.
 *
 * @abstract
 * @return {string[]} Language codes
 */
ve.init.Platform.prototype.getLanguageCodes = null;

/**
 * Check if a language code is known to this platform.
 *
 * @param {string} code Language code
 * @return {boolean} Language code is known
 */
ve.init.Platform.prototype.hasLanguageCode = function ( code ) {
	return this.getLanguageCodes().indexOf( code ) !== -1;
};

/**
 * Get a language's name from its code, in the current user language if possible.
 *
 * @abstract
 * @param {string} code Language code
 * @return {string} Language name
 */
ve.init.Platform.prototype.getLanguageName = null;

/**
 * Get a language's autonym from its code.
 *
 * @abstract
 * @param {string} code Language code
 * @return {string} Language autonym
 */
ve.init.Platform.prototype.getLanguageAutonym = null;

/**
 * Get a language's direction from its code.
 *
 * @abstract
 * @param {string} code Language code
 * @return {string} Language direction
 */
ve.init.Platform.prototype.getLanguageDirection = null;

/**
 * Initialize the platform. The default implementation is to do nothing and return a resolved
 * promise. Subclasses should override this if they have asynchronous initialization work to do.
 * The promise rejects if the platform is incompatible.
 *
 * External callers should not call this. Instead, call #getInitializedPromise.
 *
 * @private
 * @return {jQuery.Promise} Promise that will be resolved once initialization is done
 */
ve.init.Platform.prototype.initialize = function () {
	if ( !VisualEditorSupportCheck() ) {
		return ve.createDeferred().reject().promise();
	}
	return ve.createDeferred().resolve().promise();
};

/**
 * Get a promise to track when the platform has initialized. The platform won't be ready for use
 * until this promise is resolved.
 *
 * Since the initialization only happens once, and the same (resolved) promise
 * is returned when called again, and since the Platform instance is global
 * (shared between different Target instances) it is important not to rely
 * on this promise being asynchronous.
 *
 * @return {jQuery.Promise} Promise that will be resolved once the platform is ready
 */
ve.init.Platform.prototype.getInitializedPromise = function () {
	if ( !this.initialized ) {
		this.initialized = this.initialize();
	}
	return this.initialized;
};

/**
 * Post-process the symbol list.
 *
 * If a keyed object format is used, it is coverted to an array,
 * and the label property is set from the key when required.
 *
 * For individual symbols, turns the `source` property into a CSS class.
 *
 * @param {Object|Array} symbols Symbol data
 * @return {Object[]}
 */
ve.init.Platform.prototype.processSpecialCharSymbols = function ( symbols ) {
	let symbolList;
	if ( Array.isArray( symbols ) ) {
		symbolList = ve.copy( symbols );
	} else {
		symbolList = [];
		Object.keys( symbols ).forEach( ( key ) => {
			const val = symbols[ key ];
			if ( typeof val === 'object' ) {
				const symbolData = ve.copy( val );
				if ( !Object.prototype.hasOwnProperty.call( symbolData, 'label' ) ) {
					symbolData.label = key;
				}
				symbolList.push( symbolData );
			} else if ( key !== val ) {
				symbolList.push( {
					label: key,
					string: val
				} );
			} else {
				// Plain string
				symbolList.push( val );
			}
		} );
	}
	symbolList.forEach( ( symbol ) => {
		if ( symbol.source ) {
			symbol.classes = symbol.classes || [];
			symbol.classes.push( 've-ui-specialCharacterDialog-source' );
		}
	} );
	return symbolList;
};

/**
 * Fetch the special character list object
 *
 * Returns a promise which resolves with the character list
 *
 * @return {jQuery.Promise}
 */
ve.init.Platform.prototype.fetchSpecialCharList = function () {
	function tryParseJSON( json ) {
		try {
			return JSON.parse( json );
		} catch ( err ) {
			// There was no character list found, or the character list message is
			// invalid json string.
			ve.log( 've.init.Platform: Could not parse the special character list ' + json );
			ve.log( err.message );
		}
		return null;
	}

	const charsObj = {},
		groups = [ 'accents', 'mathematical', 'symbols' ];

	groups.forEach( ( group ) => {
		// The following messages are used here:
		// * visualeditor-specialcharacter-group-set-accents
		// * visualeditor-specialcharacter-group-set-mathematical
		// * visualeditor-specialcharacter-group-set-symbols
		const symbols = tryParseJSON( ve.msg( 'visualeditor-specialcharacter-group-set-' + group ) );
		if ( symbols ) {
			charsObj[ group ] = {
				// The following messages are used here:
				// * visualeditor-specialcharacter-group-label-accents
				// * visualeditor-specialcharacter-group-label-mathematical
				// * visualeditor-specialcharacter-group-label-symbols
				label: ve.msg( 'visualeditor-specialcharacter-group-label-' + group ),
				symbols: this.processSpecialCharSymbols( symbols )
			};
		}
	} );

	// This implementation always resolves instantly
	return ve.createDeferred().resolve( charsObj ).promise();
};

/**
 * Decode HTML entities for insertion into the document
 *
 * @method
 * @param {string} html HTML string
 * @return {string}
 */
ve.init.Platform.prototype.decodeEntities = ve.safeDecodeEntities;