( function () {
	/**
	 * @classdesc Upload to a wiki. Most of the functionality is implemented
	 * in {@link mw.Api#upload} and friends, but this model class will tie it
	 * together as well as let you perform actions in a logical way.
	 *
	 * A simple example:
	 * ```
	 * var file = new OO.ui.SelectFileWidget(),
	 *   button = new OO.ui.ButtonWidget( { label: 'Save' } ),
	 *   upload = new mw.Upload;
	 *
	 * button.on( 'click', () => {
	 *   upload.setFile( file.getValue() );
	 *   upload.setFilename( file.getValue().name );
	 *   upload.upload();
	 * } );
	 *
	 * $( document.body ).append( file.$element, button.$element );
	 * ```
	 * You can also choose to {@link mw.Upload#uploadToStash stash the upload}
	 * and {@link mw.Upload#finishStashUpload finalize} it later:
	 * ```
	 * var file, // Some file object
	 *   upload = new mw.Upload,
	 *   stashPromise = $.Deferred();
	 *
	 * upload.setFile( file );
	 * upload.uploadToStash().then( () => {
	 *   stashPromise.resolve();
	 * } );
	 *
	 * stashPromise.then( () => {
	 *   upload.setFilename( 'foo' );
	 *   upload.setText( 'bar' );
	 *   upload.finishStashUpload().then( () => {
	 *     console.log( 'Done!' );
	 *   } );
	 * } );
	 * ```
	 * @class mw.Upload
	 *
	 * @constructor
	 * @description Used to represent an upload in progress on the frontend.
	 * @param {Object|mw.Api} [apiconfig] A mw.Api object (or subclass), or configuration
	 *     to pass to the constructor of mw.Api.
	 */
	function Upload( apiconfig ) {
		this.api = ( apiconfig instanceof mw.Api ) ? apiconfig : new mw.Api( apiconfig );

		this.watchlist = false;
		this.text = '';
		this.comment = '';
		this.filename = null;
		this.file = null;
		this.setState( Upload.State.NEW );

		this.imageinfo = undefined;
	}

	const UP = Upload.prototype;

	/**
	 * Get the mw.Api instance used by this Upload object.
	 *
	 * @name mw.Upload.prototype.getApi
	 * @method
	 * @return {jQuery.Promise<mw.Api>}
	 */
	UP.getApi = function () {
		return $.Deferred().resolve( this.api ).promise();
	};

	/**
	 * Set the text of the file page, to be created on file upload.
	 *
	 * @name mw.Upload.prototype.setText
	 * @method
	 * @param {string} text
	 */
	UP.setText = function ( text ) {
		this.text = text;
	};

	/**
	 * Set the filename, to be finalized on upload.
	 *
	 * @name mw.Upload.prototype.setFilename
	 * @method
	 * @param {string} filename
	 */
	UP.setFilename = function ( filename ) {
		this.filename = filename;
	};

	/**
	 * Set the stashed file to finish uploading.
	 *
	 * @name mw.Upload.prototype.setFilekey
	 * @method
	 * @param {string} filekey
	 */
	UP.setFilekey = function ( filekey ) {
		this.setState( Upload.State.STASHED );
		this.stashPromise = $.Deferred().resolve( ( data ) => this.api.uploadFromStash( filekey, data ) );
	};

	/**
	 * Sets the filename based on the filename as it was on the upload.
	 *
	 * @name mw.Upload.prototype.setFilenameFromFile
	 * @method
	 */
	UP.setFilenameFromFile = function () {
		const file = this.getFile();
		if ( !file ) {
			return;
		}
		if ( file.nodeType && file.nodeType === Node.ELEMENT_NODE ) {
			// File input element, use getBasename to cut out the path
			this.setFilename( this.getBasename( file.value ) );
		} else if ( file.name ) {
			// HTML5 FileAPI File object, but use getBasename to be safe
			this.setFilename( this.getBasename( file.name ) );
		} else {
			// If we ever implement uploading files from clipboard, they might not have a name
			this.setFilename( '?' );
		}
	};

	/**
	 * Set the file to be uploaded.
	 *
	 * @name mw.Upload.prototype.setFile
	 * @method
	 * @param {HTMLInputElement|File|Blob} file
	 */
	UP.setFile = function ( file ) {
		this.file = file;
	};

	/**
	 * Set whether the file should be watchlisted after upload.
	 *
	 * @name mw.Upload.prototype.setWatchlist
	 * @method
	 * @param {boolean} watchlist
	 */
	UP.setWatchlist = function ( watchlist ) {
		this.watchlist = watchlist;
	};

	/**
	 * Set the edit comment for the upload.
	 *
	 * @name mw.Upload.prototype.setComment
	 * @method
	 * @param {string} comment
	 */
	UP.setComment = function ( comment ) {
		this.comment = comment;
	};

	/**
	 * Get the text of the file page, to be created on file upload.
	 *
	 * @name mw.Upload.prototype.getText
	 * @method
	 * @return {string}
	 */
	UP.getText = function () {
		return this.text;
	};

	/**
	 * Get the filename, to be finalized on upload.
	 *
	 * @name mw.Upload.prototype.getFilename
	 * @method
	 * @return {string}
	 */
	UP.getFilename = function () {
		return this.filename;
	};

	/**
	 * Get the file being uploaded.
	 *
	 * @name mw.Upload.prototype.getFile
	 * @method
	 * @return {HTMLInputElement|File|Blob}
	 */
	UP.getFile = function () {
		return this.file;
	};

	/**
	 * Get the boolean for whether the file will be watchlisted after upload.
	 *
	 * @name mw.Upload.prototype.getWatchlist
	 * @method
	 * @return {boolean}
	 */
	UP.getWatchlist = function () {
		return this.watchlist;
	};

	/**
	 * Get the current value of the edit comment for the upload.
	 *
	 * @name mw.Upload.prototype.getComment
	 * @method
	 * @return {string}
	 */
	UP.getComment = function () {
		return this.comment;
	};

	/**
	 * Gets the base filename from a path name.
	 *
	 * @name mw.Upload.prototype.getBasename
	 * @method
	 * @param {string} path
	 * @return {string}
	 */
	UP.getBasename = function ( path ) {
		if ( path === undefined || path === null ) {
			return '';
		}

		// Find the index of the last path separator in the
		// path, and add 1. Then, take the entire string after that.
		return path.slice(
			Math.max(
				path.lastIndexOf( '/' ),
				path.lastIndexOf( '\\' )
			) + 1
		);
	};

	/**
	 * Sets the state and state details (if any) of the upload.
	 *
	 * @name mw.Upload.prototype.setState
	 * @method
	 * @param {mw.Upload.State} state
	 * @param {Object} stateDetails
	 */
	UP.setState = function ( state, stateDetails ) {
		this.state = state;
		this.stateDetails = stateDetails;
	};

	/**
	 * Gets the state of the upload.
	 *
	 * @name mw.Upload.prototype.getState
	 * @method
	 * @return {mw.Upload.State}
	 */
	UP.getState = function () {
		return this.state;
	};

	/**
	 * Gets details of the current state.
	 *
	 * @name mw.Upload.prototype.getStateDetails
	 * @method
	 * @return {string}
	 */
	UP.getStateDetails = function () {
		return this.stateDetails;
	};

	/**
	 * Get the imageinfo object for the finished upload.
	 * Only available once the upload is finished! Don't try to get it
	 * beforehand.
	 *
	 * @name mw.Upload.prototype.getImageInfo
	 * @method
	 * @return {Object|undefined}
	 */
	UP.getImageInfo = function () {
		return this.imageinfo;
	};

	/**
	 * Upload the file directly.
	 *
	 * @name mw.Upload.prototype.upload
	 * @method
	 * @return {jQuery.Promise}
	 */
	UP.upload = function () {
		if ( !this.getFile() ) {
			return $.Deferred().reject( 'No file to upload. Call setFile to add one.' );
		}

		if ( !this.getFilename() ) {
			return $.Deferred().reject( 'No filename set. Call setFilename to add one.' );
		}

		this.setState( Upload.State.UPLOADING );

		return this.api.chunkedUpload( this.getFile(), {
			watchlist: ( this.getWatchlist() ) ? 1 : undefined,
			comment: this.getComment(),
			filename: this.getFilename(),
			text: this.getText()
		} ).then( ( result ) => {
			this.setState( Upload.State.UPLOADED );
			this.imageinfo = result.upload.imageinfo;
			return result;
		}, ( errorCode, result ) => {
			if ( result && result.upload && result.upload.warnings ) {
				this.setState( Upload.State.WARNING, result );
			} else {
				this.setState( Upload.State.ERROR, result );
			}
			return $.Deferred().reject( errorCode, result );
		} );
	};

	/**
	 * Upload the file to the stash to be completed later.
	 *
	 * @name mw.Upload.prototype.uploadToStash
	 * @method
	 * @return {jQuery.Promise}
	 */
	UP.uploadToStash = function () {
		if ( !this.getFile() ) {
			return $.Deferred().reject( 'No file to upload. Call setFile to add one.' );
		}

		if ( !this.getFilename() ) {
			this.setFilenameFromFile();
		}

		this.setState( Upload.State.UPLOADING );

		this.stashPromise = this.api.chunkedUploadToStash( this.getFile(), {
			ignorewarnings: true,
			filename: this.getFilename()
		} ).then( ( finishStash ) => {
			this.setState( Upload.State.STASHED );
			return finishStash;
		}, ( errorCode, result ) => {
			if ( result && result.upload && result.upload.warnings ) {
				this.setState( Upload.State.WARNING, result );
			} else {
				this.setState( Upload.State.ERROR, result );
			}
			return $.Deferred().reject( errorCode, result );
		} );

		return this.stashPromise;
	};

	/**
	 * Finish a stash upload.
	 *
	 * @name mw.Upload.prototype.finishStashUpload
	 * @method
	 * @return {jQuery.Promise}
	 */
	UP.finishStashUpload = function () {
		if ( !this.stashPromise ) {
			return $.Deferred().reject( 'This upload has not been stashed, please upload it to the stash first.' );
		}

		return this.stashPromise.then( ( finishStash ) => {
			this.setState( Upload.State.UPLOADING );

			return finishStash( {
				ignorewarnings: false,
				watchlist: ( this.getWatchlist() ) ? 1 : undefined,
				comment: this.getComment(),
				filename: this.getFilename(),
				text: this.getText()
			} ).then( ( result ) => {
				this.setState( Upload.State.UPLOADED );
				this.imageinfo = result.upload.imageinfo;
				return result;
			}, ( errorCode, result ) => {
				if ( result && result.upload && result.upload.warnings ) {
					this.setState( Upload.State.WARNING, result );
				} else {
					this.setState( Upload.State.ERROR, result );
				}
				return $.Deferred().reject( errorCode, result );
			} );
		} );
	};

	mw.Upload = Upload;

	/**
	 * @enum
	 *
	 * State of uploads represented in simple terms.
	 */
	mw.Upload.State = {
		/** Upload not yet started */
		NEW: 0,

		/** Upload finished, but there was a warning */
		WARNING: 1,

		/** Upload finished, but there was an error */
		ERROR: 2,

		/** Upload in progress */
		UPLOADING: 3,

		/** Upload finished, but not published, call #finishStashUpload */
		STASHED: 4,

		/** Upload finished and published */
		UPLOADED: 5
	};
}() );
