( function () {
	/**
	 * A class defining Echo API instructions and network operations
	 *
	 * @class
	 *
	 * @constructor
	 * @param {Object} config Configuration options
	 * @param {number} [config.limit=25] Number of notifications to fetch
	 */
	mw.echo.api.EchoApi = function MwEchoApiEchoApi( config ) {
		config = config || {};

		this.network = new mw.echo.api.NetworkHandler( config );

		this.fetchingPromise = null;
		this.limit = config.limit || 25;
		this.fetchingPrioritizer = new mw.echo.api.PromisePrioritizer();
	};

	OO.initClass( mw.echo.api.EchoApi );

	/**
	 * Register a set of foreign sources.
	 *
	 * @param {Object} sources Object mapping source names to config objects
	 * @param {boolean} [unreadOnly=false] Fetch only unread notifications
	 * @param {number} [limit] Specific limit of notifications. Defaults to
	 *  the default limit stated in the class.
	 */
	mw.echo.api.EchoApi.prototype.registerForeignSources = function ( sources, unreadOnly, limit ) {
		limit = limit || this.limit;

		for ( const s in sources ) {
			this.network.setApiHandler( s, new mw.echo.api.ForeignAPIHandler( sources[ s ].url, {
				unreadOnly: !!unreadOnly,
				limit: limit
			} ) );
		}
	};

	/**
	 * Register a set of local sources.
	 *
	 * @param {string[]} sources An array of source names
	 */
	mw.echo.api.EchoApi.prototype.registerLocalSources = function ( sources ) {
		const localHandler = this.network.getApiHandler( 'local' );

		for ( let i = 0; i < sources.length; i++ ) {
			this.network.setApiHandler( sources[ i ], localHandler );
		}
	};

	/**
	 * Fetch all pages with unread notifications in them per wiki
	 *
	 * @param {string[]} [sources=all] Requested sources
	 * @return {jQuery.Promise} Promise that is resolved with an object
	 *  of pages with the number of unread notifications per wiki
	 */
	mw.echo.api.EchoApi.prototype.fetchUnreadNotificationPages = function ( sources ) {
		return this.network.getApiHandler( 'local' ).fetchUnreadNotificationPages( sources )
			.then( ( data ) => OO.getProp( data, 'query', 'unreadnotificationpages' ) );
	};

	/**
	 * Fetch notifications from a given source with given filters
	 *
	 * @param {string} type Notification type to fetch: 'alert', 'message', or 'all'
	 * @param {string} [source] The source from which to fetch the notifications.
	 *  If not given, the local notifications will be fetched.
	 * @param {Object} [filters] Filter values
	 * @return {jQuery.Promise} Promise that is resolved with all notifications for the
	 *  requested types.
	 */
	mw.echo.api.EchoApi.prototype.fetchFilteredNotifications = function ( type, source, filters ) {
		source = source || 'local';

		if ( source === 'local' ) {
			return this.fetchNotifications( type, source, true, filters );
		} else {
			return this.fetchNotificationsFromRemoteSource( type, source, true, filters );
		}
	};

	/**
	 * Convert the filter object to the relevant API parameters.
	 *
	 * @param {Object} [filterObject] The filter object
	 * @param {string} [filterObject.continue] A continue variable
	 *  defining the offset to fetch notifications
	 * @param {string} [filterObject.readState] Notification read
	 *  state, 'all', 'read' or 'unread'
	 * @param {boolean} [filterObject.unreadFirst] Fetch unread notifications
	 *  first in the sorting order.
	 * @param {boolean} [filterObject.bundle] Bundle local notifications
	 * @param {string|string[]} [filterObject.titles] Requested titles. To request notifications with no title,
	 *  use null (standalone or as an array element).
	 * @return {Object} API parameter definitions to override
	 */
	mw.echo.api.EchoApi.prototype.convertFiltersToAPIParams = function ( filterObject ) {
		const overrideParams = {};

		filterObject = filterObject || {};

		if ( filterObject.continue ) {
			overrideParams.notcontinue = filterObject.continue;
		}

		if ( filterObject.unreadFirst ) {
			overrideParams.notunreadfirst = 1;
		}

		if ( filterObject.bundle ) {
			overrideParams.notbundle = 1;
		}

		if ( filterObject.readState && filterObject.readState !== 'all' ) {
			overrideParams.notfilter = filterObject.readState === 'read' ?
				'read' :
				'!read';
		}

		if ( filterObject.titles ) {
			const titles = Array.isArray( filterObject.titles ) ? filterObject.titles : [ filterObject.titles ];
			if ( titles.indexOf( null ) !== -1 ) {
				// Map null to '[]'
				titles.splice( titles.indexOf( null ), 1, '[]' );
			}
			overrideParams.nottitles = titles;
		}

		return overrideParams;
	};

	/**
	 * Fetch remote notifications from a given source. This skips the local fetching that is
	 * usually done and calls the remote wiki directly.
	 *
	 * @param {string} type Notification type to fetch: 'alert', 'message', or 'all'
	 * @param {string|string[]} [source] The source from which to fetch the notifications.
	 *  If not given, the local notifications will be fetched.
	 * @param {boolean} [isForced] Force a refresh on the fetch notifications promise
	 * @param {Object} [filters] Filter values
	 * @return {jQuery.Promise} Promise that is resolved with all notifications for the
	 *  requested types.
	 */
	mw.echo.api.EchoApi.prototype.fetchNotificationsFromRemoteSource = function ( type, source, isForced, filters ) {
		const handler = this.network.getApiHandler( source );

		if ( !handler ) {
			return $.Deferred().reject().promise();
		}

		return this.fetchingPrioritizer.prioritize( handler.fetchNotifications(
			type,
			// For the remote source, we are fetching 'local' notifications
			'local',
			!!isForced,
			this.convertFiltersToAPIParams( filters )
		) )
			.then( ( result ) => OO.getProp( result.query, 'notifications' ) );
	};

	/**
	 * Fetch notifications from the server based on type
	 *
	 * @param {string} type Notification type to fetch: 'alert', 'message', or 'all'
	 * @param {string|string[]} [sources] The source from which to fetch the notifications.
	 *  If not given, the local notifications will be fetched.
	 * @param {boolean} [isForced] Force a refresh on the fetch notifications promise
	 * @param {Object} [filters] Filter values
	 * @return {jQuery.Promise} Promise that is resolved with all notifications for the
	 *  requested types.
	 */
	mw.echo.api.EchoApi.prototype.fetchNotifications = function ( type, sources, isForced, filters ) {
		sources = Array.isArray( sources ) ?
			sources :
			sources ?
				[ sources ] :
				'local';

		return this.fetchingPrioritizer.prioritize( this.network.getApiHandler( 'local' ).fetchNotifications(
			type,
			sources,
			isForced,
			this.convertFiltersToAPIParams( filters )
		) )
			.then( ( result ) => OO.getProp( result.query, 'notifications' ) );
	};

	/**
	 * Fetch notifications from several sources
	 *
	 * @param {string[]} sourceArray An array of sources to fetch from the group
	 * @param {string} type Notification type
	 * @param {boolean} bundle Bundle local notifications
	 * @return {jQuery.Promise} A promise that resolves with an object that maps wiki
	 *  names to an array of their items' API data objects.
	 */
	mw.echo.api.EchoApi.prototype.fetchNotificationGroups = function ( sourceArray, type, bundle ) {
		const overrideParams = { notcrosswikisummary: false, notbundle: bundle };
		return this.network.getApiHandler( 'local' ).fetchNotifications( type, sourceArray, true, overrideParams )
			.then( ( result ) => {
				const items = OO.getProp( result, 'query', 'notifications', 'list' ),
					groups = {};

				// Split the items to groups
				for ( let i = 0; i < items.length; i++ ) {
					groups[ items[ i ].wiki ] = groups[ items[ i ].wiki ] || [];
					groups[ items[ i ].wiki ].push( items[ i ] );
				}

				return groups;
			} );
	};

	/**
	 * Mark items as read in the API.
	 *
	 * @param {string[]} itemIds An array of item IDs to mark as read
	 * @param {string} source The source that these items belong to
	 * @param {boolean} [isRead] The read state of the item; true for marking the
	 *  item as read, false for marking the item as unread
	 * @return {jQuery.Promise} A promise that is resolved when the operation
	 *  is complete, with the number of unread notifications still remaining
	 *  for that type in the given source
	 */
	mw.echo.api.EchoApi.prototype.markItemsRead = function ( itemIds, source, isRead ) {
		// markasread is proxied via the local API
		return this.network.getApiHandler( 'local' ).markItemsRead( source, itemIds, isRead );
	};

	/**
	 * Mark all notifications for a given type as read in the given source.
	 *
	 * @param {string} source Symbolic name of notifications source
	 * @param {string} type Notifications type
	 * @return {jQuery.Promise} A promise that is resolved when the operation
	 *  is complete, with the number of unread notifications still remaining
	 *  for that type in the given source
	 */
	mw.echo.api.EchoApi.prototype.markAllRead = function ( source, type ) {
		// markasread is proxied via the local API
		return this.network.getApiHandler( 'local' ).markAllRead( source, type );
	};

	/**
	 * Fetch the number of unread notifications for the given type in the given
	 * source.
	 *
	 * @param {string} source Notifications source
	 * @param {string} type Notification type
	 * @param {boolean} [localOnly] Fetches only the count of local notifications,
	 *  and ignores cross-wiki notifications.
	 * @return {jQuery.Promise} A promise that is resolved with the number of
	 *  unread notifications for the given type and source.
	 */
	mw.echo.api.EchoApi.prototype.fetchUnreadCount = function ( source, type, localOnly ) {
		return this.network.getApiHandler( source ).fetchUnreadCount( type, localOnly );
	};

	/**
	 * Update the seenTime property for the given type.
	 * We only need to update this in a single source for the seenTime
	 * to be updated globally - but we will let the consumer of
	 * this method override the choice of which source to update.
	 *
	 * @param {string} [type='alert,message'] Notification type
	 * @param {string} [source='local'] Notification source
	 * @return {jQuery.Promise} A promise that is resolved when the operation is complete.
	 */
	mw.echo.api.EchoApi.prototype.updateSeenTime = function ( type, source ) {
		source = source || 'local';
		type = type || [ 'alert', 'message' ];

		return this.network.getApiHandler( source ).updateSeenTime( type );
	};

	/**
	 * Send a general query to the API. This is mostly for dynamic actions
	 * where other extensions may set up API actions that are unique and
	 * unanticipated.
	 *
	 * @param {Object} params API parameters
	 * @param {string} [source='local'] Requested source to query
	 * @return {jQuery.Promise} Promise that is resolved when the action
	 *  is complete
	 */
	mw.echo.api.EchoApi.prototype.queryAPI = function ( params, source ) {
		source = source || 'local';
		return this.network.getApiHandler( source )
			.queryAPI( params );
	};

	/**
	 * Check whether the API promise for fetch notification is in an error
	 * state for the given source and notification type.
	 *
	 * @param {string} source Notification source.
	 * @param {string} type Notification type
	 * @return {boolean} The API response for fetching notification has
	 *  resolved in an error state, or is rejected.
	 */
	mw.echo.api.EchoApi.prototype.isFetchingErrorState = function ( source, type ) {
		return this.network.getApiHandler( source ).isFetchingErrorState( type, [ source ] );
	};

	/**
	 * Get the fetch notifications promise active for the current source and type.
	 *
	 * @param {string} source Notification source.
	 * @param {string} type Notification type
	 * @return {jQuery.Promise} Promise that is resolved when notifications are
	 *  fetched from the API.
	 */
	mw.echo.api.EchoApi.prototype.getFetchNotificationPromise = function ( source, type ) {
		return this.network.getApiHandler( source ).getFetchNotificationPromise( type );
	};

	/**
	 * Get the set limit for fetching notifications per request
	 *
	 * @return {number} Limit of notifications per request
	 */
	mw.echo.api.EchoApi.prototype.getLimit = function () {
		return this.limit;
	};
}() );
