/**
 * RequestManager is a mixin that manages the lifecycle of a promise-backed request for a widget,
 * such as the {@link OO.ui.mixin.LookupElement}.
 *
 * @class
 * @abstract
 *
 * @constructor
 * @param {Object} [config] Configuration options
 * @param {boolean} [config.showPendingRequest=true] Show pending state while request data is being fetched.
 *  Requires widget to have also mixed in {@link OO.ui.mixin.PendingElement}.
 */
OO.ui.mixin.RequestManager = function OoUiMixinRequestManager( config ) {
	this.requestCache = {};
	this.requestQuery = null;
	this.requestRequest = null;
	this.showPendingRequest = !!this.pushPending && config.showPendingRequest !== false;
};

/* Setup */

OO.initClass( OO.ui.mixin.RequestManager );

/**
 * Get request results for the current query.
 *
 * @return {jQuery.Promise} Promise object which will be passed response data as the first argument
 *  of the done event. If the request was aborted to make way for a subsequent request, this
 *  promise may not be rejected, depending on what jQuery feels like doing.
 */
OO.ui.mixin.RequestManager.prototype.getRequestData = function () {
	const value = this.getRequestQuery(),
		deferred = $.Deferred();

	this.abortRequest();
	if ( Object.prototype.hasOwnProperty.call( this.requestCache, value ) ) {
		deferred.resolve( this.requestCache[ value ] );
	} else {
		if ( this.showPendingRequest ) {
			this.pushPending();
		}
		this.requestQuery = value;
		const ourRequest = this.requestRequest = this.getRequest();
		ourRequest
			.always( () => {
				// We need to pop pending even if this is an old request, otherwise
				// the widget will remain pending forever.
				// TODO: this assumes that an aborted request will fail or succeed soon after
				// being aborted, or at least eventually. It would be nice if we could popPending()
				// at abort time, but only if we knew that we hadn't already called popPending()
				// for that request.
				if ( this.showPendingRequest ) {
					this.popPending();
				}
			} )
			.done( ( response ) => {
				// If this is an old request (and aborting it somehow caused it to still succeed),
				// ignore its success completely
				if ( ourRequest === this.requestRequest ) {
					this.requestQuery = null;
					this.requestRequest = null;
					this.requestCache[ value ] =
						this.getRequestCacheDataFromResponse( response );
					deferred.resolve( this.requestCache[ value ] );
				}
			} )
			.fail( () => {
				// If this is an old request (or a request failing because it's being aborted),
				// ignore its failure completely
				if ( ourRequest === this.requestRequest ) {
					this.requestQuery = null;
					this.requestRequest = null;
					deferred.reject();
				}
			} );
	}
	return deferred.promise();
};

/**
 * Abort the currently pending request, if any.
 *
 * @private
 */
OO.ui.mixin.RequestManager.prototype.abortRequest = function () {
	const oldRequest = this.requestRequest;
	if ( oldRequest ) {
		// First unset this.requestRequest to the fail handler will notice
		// that the request is no longer current
		this.requestRequest = null;
		this.requestQuery = null;
		oldRequest.abort();
	}
};

/**
 * Get the query to be made.
 *
 * @protected
 * @method
 * @abstract
 * @return {string} query to be used
 */
OO.ui.mixin.RequestManager.prototype.getRequestQuery = null;

/**
 * Get a new request object of the current query value.
 *
 * @protected
 * @method
 * @abstract
 * @return {jQuery.Promise} jQuery AJAX object, or promise object with an .abort() method
 */
OO.ui.mixin.RequestManager.prototype.getRequest = null;

/**
 * Pre-process data returned by the request from #getRequest.
 *
 * The return value of this function will be cached, and any further queries for the given value
 * will use the cache rather than doing API requests.
 *
 * @protected
 * @method
 * @abstract
 * @param {any} response Response from server
 * @return {any} Cached result data
 */
OO.ui.mixin.RequestManager.prototype.getRequestCacheDataFromResponse = null;