/*
 * jQuery File Upload Plugin 5.34.0
 * https://github.com/blueimp/jQuery-File-Upload
 *
 * Copyright 2010, Sebastian Tschan
 * https://blueimp.net
 *
 * Licensed under the MIT license:
 * http://www.opensource.org/licenses/MIT
 */

/* jslint nomen: true, unparam: true, regexp: true */
/* global define, window, document, location, File, Blob, FormData */

(function (factory) {
  if (typeof define === 'function' && define.amd) {
    // Register as an anonymous AMD module:
    define([
      'jquery',
      'jquery.ui.widget',
    ], factory);
  } else {
    // Browser globals:
    factory(window.jQuery);
  }
}(($) => {
  // Detect file input support, based on
  // http://viljamis.com/blog/2012/file-upload-support-on-mobile/
  $.support.fileInput = !(new RegExp(
    // Handle devices which give false positives for the feature detection:
    '(Android (1\\.[0156]|2\\.[01]))'
            + '|(Windows Phone (OS 7|8\\.0))|(XBLWP)|(ZuneWP)|(WPDesktop)'
            + '|(w(eb)?OSBrowser)|(webOS)'
            + '|(Kindle/(1\\.0|2\\.[05]|3\\.0))',
  ).test(window.navigator.userAgent)
        // Feature detection for all other devices:
        || $('<input type="file">').prop('disabled'));

  // The FileReader API is not actually used, but works as feature detection,
  // as e.g. Safari supports XHR file uploads via the FormData API,
  // but not non-multipart XHR file uploads:
  $.support.xhrFileUpload = !!(window.XMLHttpRequestUpload && window.FileReader);
  $.support.xhrFormDataFileUpload = !!window.FormData;

  // Detect support for Blob slicing (required for chunked uploads):
  $.support.blobSlice = window.Blob && (Blob.prototype.slice
        || Blob.prototype.webkitSlice || Blob.prototype.mozSlice);

  // The fileupload widget listens for change events on file input fields defined
  // via fileInput setting and paste or drop events of the given dropZone.
  // In addition to the default jQuery Widget methods, the fileupload widget
  // exposes the "add" and "send" methods, to add or directly send files using
  // the fileupload API.
  // By default, files added via file input selection, paste, drag & drop or
  // "add" method are uploaded immediately, but it is possible to override
  // the "add" callback option to queue file uploads.
  $.widget('blueimp.fileupload', {

    options: {
      // The drop target element(s), by the default the complete document.
      // Set to null to disable drag & drop support:
      dropZone: $(document),
      // The paste target element(s), by the default the complete document.
      // Set to null to disable paste support:
      pasteZone: $(document),
      // The file input field(s), that are listened to for change events.
      // If undefined, it is set to the file input fields inside
      // of the widget element on plugin initialization.
      // Set to null to disable the change listener.
      fileInput: undefined,
      // By default, the file input field is replaced with a clone after
      // each input field change event. This is required for iframe transport
      // queues and allows change events to be fired for the same file
      // selection, but can be disabled by setting the following option to false:
      replaceFileInput: true,
      // The parameter name for the file form data (the request argument name).
      // If undefined or empty, the name property of the file input field is
      // used, or "files[]" if the file input name property is also empty,
      // can be a string or an array of strings:
      paramName: undefined,
      // By default, each file of a selection is uploaded using an individual
      // request for XHR type uploads. Set to false to upload file
      // selections in one request each:
      singleFileUploads: true,
      // To limit the number of files uploaded with one XHR request,
      // set the following option to an integer greater than 0:
      limitMultiFileUploads: undefined,
      // Set the following option to true to issue all file upload requests
      // in a sequential order:
      sequentialUploads: false,
      // To limit the number of concurrent uploads,
      // set the following option to an integer greater than 0:
      limitConcurrentUploads: undefined,
      // Set the following option to true to force iframe transport uploads:
      forceIframeTransport: false,
      // Set the following option to the location of a redirect url on the
      // origin server, for cross-domain iframe transport uploads:
      redirect: undefined,
      // The parameter name for the redirect url, sent as part of the form
      // data and set to 'redirect' if this option is empty:
      redirectParamName: undefined,
      // Set the following option to the location of a postMessage window,
      // to enable postMessage transport uploads:
      postMessage: undefined,
      // By default, XHR file uploads are sent as multipart/form-data.
      // The iframe transport is always using multipart/form-data.
      // Set to false to enable non-multipart XHR uploads:
      multipart: true,
      // To upload large files in smaller chunks, set the following option
      // to a preferred maximum chunk size. If set to 0, null or undefined,
      // or the browser does not support the required Blob API, files will
      // be uploaded as a whole.
      maxChunkSize: undefined,
      // When a non-multipart upload or a chunked multipart upload has been
      // aborted, this option can be used to resume the upload by setting
      // it to the size of the already uploaded bytes. This option is most
      // useful when modifying the options object inside of the "add" or
      // "send" callbacks, as the options are cloned for each file upload.
      uploadedBytes: undefined,
      // By default, failed (abort or error) file uploads are removed from the
      // global progress calculation. Set the following option to false to
      // prevent recalculating the global progress data:
      recalculateProgress: true,
      // Interval in milliseconds to calculate and trigger progress events:
      progressInterval: 100,
      // Interval in milliseconds to calculate progress bitrate:
      bitrateInterval: 500,
      // By default, uploads are started automatically when adding files:
      autoUpload: true,

      // Error and info messages:
      messages: {
        uploadedBytes: 'Uploaded bytes exceed file size',
      },

      // Translation function, gets the message key to be translated
      // and an object with context specific data as arguments:
      i18n(message, context) {
        message = this.messages[message] || message.toString();
        if (context) {
          $.each(context, (key, value) => {
            message = message.replace(`{${key}}`, value);
          });
        }
        return message;
      },

      // Additional form data to be sent along with the file uploads can be set
      // using this option, which accepts an array of objects with name and
      // value properties, a function returning such an array, a FormData
      // object (for XHR file uploads), or a simple object.
      // The form of the first fileInput is given as parameter to the function:
      formData(form) {
        return form.serializeArray();
      },

      // The add callback is invoked as soon as files are added to the fileupload
      // widget (via file input selection, drag & drop, paste or add API call).
      // If the singleFileUploads option is enabled, this callback will be
      // called once for each file in the selection for XHR file uploads, else
      // once for each file selection.
      //
      // The upload starts when the submit method is invoked on the data parameter.
      // The data object contains a files property holding the added files
      // and allows you to override plugin options as well as define ajax settings.
      //
      // Listeners for this callback can also be bound the following way:
      // .bind('fileuploadadd', func);
      //
      // data.submit() returns a Promise object and allows to attach additional
      // handlers using jQuery's Deferred callbacks:
      // data.submit().done(func).fail(func).always(func);
      add(e, data) {
        if (e.isDefaultPrevented()) {
          return false;
        }
        if (data.autoUpload || (data.autoUpload !== false
                        && $(this).fileupload('option', 'autoUpload'))) {
          data.process().done(() => {
            data.submit();
          });
        }
      },

      // Other callbacks:

      // Callback for the submit event of each file upload:
      // submit: function (e, data) {}, // .bind('fileuploadsubmit', func);

      // Callback for the start of each file upload request:
      // send: function (e, data) {}, // .bind('fileuploadsend', func);

      // Callback for successful uploads:
      // done: function (e, data) {}, // .bind('fileuploaddone', func);

      // Callback for failed (abort or error) uploads:
      // fail: function (e, data) {}, // .bind('fileuploadfail', func);

      // Callback for completed (success, abort or error) requests:
      // always: function (e, data) {}, // .bind('fileuploadalways', func);

      // Callback for upload progress events:
      // progress: function (e, data) {}, // .bind('fileuploadprogress', func);

      // Callback for global upload progress events:
      // progressall: function (e, data) {}, // .bind('fileuploadprogressall', func);

      // Callback for uploads start, equivalent to the global ajaxStart event:
      // start: function (e) {}, // .bind('fileuploadstart', func);

      // Callback for uploads stop, equivalent to the global ajaxStop event:
      // stop: function (e) {}, // .bind('fileuploadstop', func);

      // Callback for change events of the fileInput(s):
      // change: function (e, data) {}, // .bind('fileuploadchange', func);

      // Callback for paste events to the pasteZone(s):
      // paste: function (e, data) {}, // .bind('fileuploadpaste', func);

      // Callback for drop events of the dropZone(s):
      // drop: function (e, data) {}, // .bind('fileuploaddrop', func);

      // Callback for dragover events of the dropZone(s):
      // dragover: function (e) {}, // .bind('fileuploaddragover', func);

      // Callback for the start of each chunk upload request:
      // chunksend: function (e, data) {}, // .bind('fileuploadchunksend', func);

      // Callback for successful chunk uploads:
      // chunkdone: function (e, data) {}, // .bind('fileuploadchunkdone', func);

      // Callback for failed (abort or error) chunk uploads:
      // chunkfail: function (e, data) {}, // .bind('fileuploadchunkfail', func);

      // Callback for completed (success, abort or error) chunk upload requests:
      // chunkalways: function (e, data) {}, // .bind('fileuploadchunkalways', func);

      // The plugin options are used as settings object for the ajax calls.
      // The following are jQuery ajax settings required for the file uploads:
      processData: false,
      contentType: false,
      cache: false,
    },

    // A list of options that require reinitializing event listeners and/or
    // special initialization code:
    _specialOptions: [
      'fileInput',
      'dropZone',
      'pasteZone',
      'multipart',
      'forceIframeTransport',
    ],

    _blobSlice: $.support.blobSlice && function () {
      const slice = this.slice || this.webkitSlice || this.mozSlice;

      return slice.apply(this, arguments);
    },

    _BitrateTimer() {
      this.timestamp = ((Date.now) ? Date.now() : (new Date()).getTime());
      this.loaded = 0;
      this.bitrate = 0;
      this.getBitrate = function (now, loaded, interval) {
        const timeDiff = now - this.timestamp;

        if (!this.bitrate || !interval || timeDiff > interval) {
          this.bitrate = (loaded - this.loaded) * (1000 / timeDiff) * 8;
          this.loaded = loaded;
          this.timestamp = now;
        }
        return this.bitrate;
      };
    },

    _isXHRUpload(options) {
      return !options.forceIframeTransport
                && ((!options.multipart && $.support.xhrFileUpload)
                || $.support.xhrFormDataFileUpload);
    },

    _getFormData(options) {
      let formData;

      if (typeof options.formData === 'function') {
        return options.formData(options.form);
      }
      if ($.isArray(options.formData)) {
        return options.formData;
      }
      if ($.type(options.formData) === 'object') {
        formData = [];
        $.each(options.formData, (name, value) => {
          formData.push({name, value});
        });
        return formData;
      }
      return [];
    },

    _getTotal(files) {
      let total = 0;
      $.each(files, (index, file) => {
        total += file.size || 1;
      });
      return total;
    },

    _initProgressObject(obj) {
      const progress = {
        loaded: 0,
        total: 0,
        bitrate: 0,
      };

      if (obj._progress) {
        $.extend(obj._progress, progress);
      } else {
        obj._progress = progress;
      }
    },

    _initResponseObject(obj) {
      let prop;

      if (obj._response) {
        for (prop in obj._response) {
          if (obj._response.hasOwnProperty(prop)) {
            delete obj._response[prop];
          }
        }
      } else {
        obj._response = {};
      }
    },

    _onProgress(e, data) {
      if (e.lengthComputable) {
        const now = ((Date.now) ? Date.now() : (new Date()).getTime());
        let loaded;

        if (data._time && data.progressInterval
                        && (now - data._time < data.progressInterval)
                        && e.loaded !== e.total) {
          return;
        }
        data._time = now;
        loaded = Math.floor(
          e.loaded / e.total * (data.chunkSize || data._progress.total),
        ) + (data.uploadedBytes || 0);
        // Add the difference from the previously loaded state
        // to the global loaded counter:
        this._progress.loaded += (loaded - data._progress.loaded);
        this._progress.bitrate = this._bitrateTimer.getBitrate(
          now,
          this._progress.loaded,
          data.bitrateInterval,
        );
        data._progress.loaded = data.loaded = loaded;
        data._progress.bitrate = data.bitrate = data._bitrateTimer.getBitrate(
          now,
          loaded,
          data.bitrateInterval,
        );
        // Trigger a custom progress event with a total data property set
        // to the file size(s) of the current upload and a loaded data
        // property calculated accordingly:
        this._trigger(
          'progress',
          $.Event('progress', {delegatedEvent: e}),
          data,
        );
        // Trigger a global progress event for all current file uploads,
        // including ajax calls queued for sequential file uploads:
        this._trigger(
          'progressall',
          $.Event('progressall', {delegatedEvent: e}),
          this._progress,
        );
      }
    },

    _initProgressListener(options) {
      const that = this;
      const xhr = options.xhr ? options.xhr() : $.ajaxSettings.xhr();

      // Accesss to the native XHR object is required to add event listeners
      // for the upload progress event:
      if (xhr.upload) {
        $(xhr.upload).bind('progress', (e) => {
          const oe = e.originalEvent;
          // Make sure the progress event properties get copied over:
          e.lengthComputable = oe.lengthComputable;
          e.loaded = oe.loaded;
          e.total = oe.total;
          that._onProgress(e, options);
        });
        options.xhr = function () {
          return xhr;
        };
      }
    },

    _isInstanceOf(type, obj) {
      // Cross-frame instanceof check
      return Object.prototype.toString.call(obj) === `[object ${type}]`;
    },

    _initXHRData(options) {
      const that = this;
      let formData;
      const file = options.files[0];
      // Ignore non-multipart setting if not supported:
      const multipart = options.multipart || !$.support.xhrFileUpload;
      const paramName = options.paramName[0];
      options.headers = $.extend({}, options.headers);
      if (options.contentRange) {
        options.headers['Content-Range'] = options.contentRange;
      }
      if (!multipart || options.blob || !this._isInstanceOf('File', file)) {
        options.headers['Content-Disposition'] = `attachment; filename="${
          encodeURI(file.name)}"`;
      }
      if (!multipart) {
        options.contentType = file.type;
        options.data = options.blob || file;
      } else if ($.support.xhrFormDataFileUpload) {
        if (options.postMessage) {
          // window.postMessage does not allow sending FormData
          // objects, so we just add the File/Blob objects to
          // the formData array and let the postMessage window
          // create the FormData object out of this array:
          formData = this._getFormData(options);
          if (options.blob) {
            formData.push({
              name: paramName,
              value: options.blob,
            });
          } else {
            $.each(options.files, (index, file) => {
              formData.push({
                name: options.paramName[index] || paramName,
                value: file,
              });
            });
          }
        } else {
          if (that._isInstanceOf('FormData', options.formData)) {
            formData = options.formData;
          } else {
            formData = new FormData();
            $.each(this._getFormData(options), (index, field) => {
              formData.append(field.name, field.value);
            });
          }
          if (options.blob) {
            formData.append(paramName, options.blob, file.name);
          } else {
            $.each(options.files, (index, file) => {
              // This check allows the tests to run with
              // dummy objects:
              if (that._isInstanceOf('File', file)
                                    || that._isInstanceOf('Blob', file)) {
                formData.append(
                  options.paramName[index] || paramName,
                  file,
                  file.name,
                );
              }
            });
          }
        }
        options.data = formData;
      }
      // Blob reference is not needed anymore, free memory:
      options.blob = null;
    },

    _initIframeSettings(options) {
      const targetHost = $('<a></a>').prop('href', options.url).prop('host');
      // Setting the dataType to iframe enables the iframe transport:
      options.dataType = `iframe ${options.dataType || ''}`;
      // The iframe transport accepts a serialized array as form data:
      options.formData = this._getFormData(options);
      // Add redirect url to form data on cross-domain uploads:
      if (options.redirect && targetHost && targetHost !== location.host) {
        options.formData.push({
          name: options.redirectParamName || 'redirect',
          value: options.redirect,
        });
      }
    },

    _initDataSettings(options) {
      if (this._isXHRUpload(options)) {
        if (!this._chunkedUpload(options, true)) {
          if (!options.data) {
            this._initXHRData(options);
          }
          this._initProgressListener(options);
        }
        if (options.postMessage) {
          // Setting the dataType to postmessage enables the
          // postMessage transport:
          options.dataType = `postmessage ${options.dataType || ''}`;
        }
      } else {
        this._initIframeSettings(options);
      }
    },

    _getParamName(options) {
      const fileInput = $(options.fileInput);
      let {paramName} = options;

      if (!paramName) {
        paramName = [];
        fileInput.each(function () {
          const input = $(this);
          const name = input.prop('name') || 'files[]';
          let i = (input.prop('files') || [1]).length;
          while (i) {
            paramName.push(name);
            i -= 1;
          }
        });
        if (!paramName.length) {
          paramName = [fileInput.prop('name') || 'files[]'];
        }
      } else if (!$.isArray(paramName)) {
        paramName = [paramName];
      }
      return paramName;
    },

    _initFormSettings(options) {
      // Retrieve missing options from the input field and the
      // associated form, if available:
      if (!options.form || !options.form.length) {
        options.form = $(options.fileInput.prop('form'));
        // If the given file input doesn't have an associated form,
        // use the default widget file input's form:
        if (!options.form.length) {
          options.form = $(this.options.fileInput.prop('form'));
        }
      }
      options.paramName = this._getParamName(options);
      if (!options.url) {
        options.url = options.form.prop('action') || location.href;
      }
      // The HTTP request method must be "POST" or "PUT":
      options.type = (options.type
                || ($.type(options.form.prop('method')) === 'string'
                    && options.form.prop('method')) || ''
      ).toUpperCase();
      if (options.type !== 'POST' && options.type !== 'PUT'
                    && options.type !== 'PATCH') {
        options.type = 'POST';
      }
      if (!options.formAcceptCharset) {
        options.formAcceptCharset = options.form.attr('accept-charset');
      }
    },

    _getAJAXSettings(data) {
      const options = $.extend({}, this.options, data);
      this._initFormSettings(options);
      this._initDataSettings(options);
      options.url = `${options.url}&rand=${new Date().getTime()}`;
      return options;
    },

    // jQuery 1.6 doesn't provide .state(),
    // while jQuery 1.8+ removed .isRejected() and .isResolved():
    _getDeferredState(deferred) {
      if (deferred.state) {
        return deferred.state();
      }
      if (deferred.isResolved()) {
        return 'resolved';
      }
      if (deferred.isRejected()) {
        return 'rejected';
      }
      return 'pending';
    },

    // Maps jqXHR callbacks to the equivalent
    // methods of the given Promise object:
    _enhancePromise(promise) {
      promise.success = promise.done;
      promise.error = promise.fail;
      promise.complete = promise.always;
      return promise;
    },

    // Creates and returns a Promise object enhanced with
    // the jqXHR methods abort, success, error and complete:
    _getXHRPromise(resolveOrReject, context, args) {
      const dfd = $.Deferred();
      const promise = dfd.promise();
      context = context || this.options.context || promise;
      if (resolveOrReject === true) {
        dfd.resolveWith(context, args);
      } else if (resolveOrReject === false) {
        dfd.rejectWith(context, args);
      }
      promise.abort = dfd.promise;
      return this._enhancePromise(promise);
    },

    // Adds convenience methods to the data callback argument:
    _addConvenienceMethods(e, data) {
      const that = this;
      const getPromise = function (data) {
        return $.Deferred().resolveWith(that, [data]).promise();
      };
      data.process = function (resolveFunc, rejectFunc) {
        if (resolveFunc || rejectFunc) {
          data._processQueue = this._processQueue = (this._processQueue || getPromise(this))
            .pipe(resolveFunc, rejectFunc);
        }
        return this._processQueue || getPromise(this);
      };
      data.submit = function () {
        if (this.state() !== 'pending') {
          data.jqXHR = this.jqXHR = (that._trigger(
            'submit',
            $.Event('submit', {delegatedEvent: e}),
            this,
          ) !== false) && that._onSend(e, this);
        }
        return this.jqXHR || that._getXHRPromise();
      };
      data.abort = function () {
        if (this.jqXHR) {
          return this.jqXHR.abort();
        }
        return that._getXHRPromise();
      };
      data.state = function () {
        if (this.jqXHR) {
          return that._getDeferredState(this.jqXHR);
        }
        if (this._processQueue) {
          return that._getDeferredState(this._processQueue);
        }
      };
      data.progress = function () {
        return this._progress;
      };
      data.response = function () {
        return this._response;
      };
    },

    // Parses the Range header from the server response
    // and returns the uploaded bytes:
    _getUploadedBytes(jqXHR) {
      const range = jqXHR.getResponseHeader('Range');
      const parts = range && range.split('-');
      const upperBytesPos = parts && parts.length > 1
                    && parseInt(parts[1], 10);

      return upperBytesPos && upperBytesPos + 1;
    },

    // Uploads a file in multiple, sequential requests
    // by splitting the file up in multiple blob chunks.
    // If the second parameter is true, only tests if the file
    // should be uploaded in chunks, but does not invoke any
    // upload requests:
    _chunkedUpload(options, testOnly) {
      options.uploadedBytes = options.uploadedBytes || 0;
      const that = this;
      const file = options.files[0];
      const fs = file.size;
      let ub = options.uploadedBytes;
      const mcs = options.maxChunkSize || fs;
      const slice = this._blobSlice;
      const dfd = $.Deferred();
      const promise = dfd.promise();
      let jqXHR;
      let upload;

      if (!(this._isXHRUpload(options) && slice && (ub || mcs < fs))
                    || options.data) {
        return false;
      }
      if (testOnly) {
        return true;
      }
      if (ub >= fs) {
        file.error = options.i18n('uploadedBytes');
        return this._getXHRPromise(
          false,
          options.context,
          [null, 'error', file.error],
        );
      }
      // The chunk upload method:
      upload = function () {
        // Clone the options object for each chunk upload:
        const o = $.extend({}, options);
        const currentLoaded = o._progress.loaded;
        o.blob = slice.call(
          file,
          ub,
          ub + mcs,
          file.type,
        );
        // Store the current chunk size, as the blob itself
        // will be dereferenced after data processing:
        o.chunkSize = o.blob.size;
        // Expose the chunk bytes position range:
        o.contentRange = `bytes ${ub}-${
          ub + o.chunkSize - 1}/${fs}`;
        // Process the upload data (the blob and potential form data):
        that._initXHRData(o);
        // Add progress listeners for this chunk upload:
        that._initProgressListener(o);
        jqXHR = ((that._trigger('chunksend', null, o) !== false && $.ajax(o))
                        || that._getXHRPromise(false, o.context))
          .done((result, textStatus, jqXHR) => {
            ub = that._getUploadedBytes(jqXHR)
                            || (ub + o.chunkSize);
            // Create a progress event if no final progress event
            // with loaded equaling total has been triggered
            // for this chunk:
            if (currentLoaded + o.chunkSize - o._progress.loaded) {
              that._onProgress($.Event('progress', {
                lengthComputable: true,
                loaded: ub - o.uploadedBytes,
                total: ub - o.uploadedBytes,
              }), o);
            }
            options.uploadedBytes = o.uploadedBytes = ub;
            o.result = result;
            o.textStatus = textStatus;
            o.jqXHR = jqXHR;
            that._trigger('chunkdone', null, o);
            that._trigger('chunkalways', null, o);
            if (ub < fs) {
              // File upload not yet complete,
              // continue with the next chunk:
              upload();
            } else {
              dfd.resolveWith(
                o.context,
                [result, textStatus, jqXHR],
              );
            }
          })
          .fail((jqXHR, textStatus, errorThrown) => {
            o.jqXHR = jqXHR;
            o.textStatus = textStatus;
            o.errorThrown = errorThrown;
            that._trigger('chunkfail', null, o);
            that._trigger('chunkalways', null, o);
            dfd.rejectWith(
              o.context,
              [jqXHR, textStatus, errorThrown],
            );
          });
      };
      this._enhancePromise(promise);
      promise.abort = function () {
        return jqXHR.abort();
      };
      upload();
      return promise;
    },

    _beforeSend(e, data) {
      if (this._active === 0) {
        // the start callback is triggered when an upload starts
        // and no other uploads are currently running,
        // equivalent to the global ajaxStart event:
        this._trigger('start');
        // Set timer for global bitrate progress calculation:
        this._bitrateTimer = new this._BitrateTimer();
        // Reset the global progress values:
        this._progress.loaded = this._progress.total = 0;
        this._progress.bitrate = 0;
      }
      // Make sure the container objects for the .response() and
      // .progress() methods on the data object are available
      // and reset to their initial state:
      this._initResponseObject(data);
      this._initProgressObject(data);
      data._progress.loaded = data.loaded = data.uploadedBytes || 0;
      data._progress.total = data.total = this._getTotal(data.files) || 1;
      data._progress.bitrate = data.bitrate = 0;
      this._active += 1;
      // Initialize the global progress values:
      this._progress.loaded += data.loaded;
      this._progress.total += data.total;
    },

    _onDone(result, textStatus, jqXHR, options) {
      const {total} = options._progress;
      const response = options._response;

      if (options._progress.loaded < total) {
        // Create a progress event if no final progress event
        // with loaded equaling total has been triggered:
        this._onProgress($.Event('progress', {
          lengthComputable: true,
          loaded: total,
          total,
        }), options);
      }
      response.result = options.result = result;
      response.textStatus = options.textStatus = textStatus;
      response.jqXHR = options.jqXHR = jqXHR;
      this._trigger('done', null, options);
    },

    _onFail(jqXHR, textStatus, errorThrown, options) {
      const response = options._response;

      if (options.recalculateProgress) {
        // Remove the failed (error or abort) file upload from
        // the global progress calculation:
        this._progress.loaded -= options._progress.loaded;
        this._progress.total -= options._progress.total;
      }
      response.jqXHR = options.jqXHR = jqXHR;
      response.textStatus = options.textStatus = textStatus;
      response.errorThrown = options.errorThrown = errorThrown;
      this._trigger('fail', null, options);
    },

    _onAlways(jqXHRorResult, textStatus, jqXHRorError, options) {
      // jqXHRorResult, textStatus and jqXHRorError are added to the
      // options object via done and fail callbacks
      this._trigger('always', null, options);
    },

    _onSend(e, data) {
      if (!data.submit) {
        this._addConvenienceMethods(e, data);
      }
      const that = this;
      let jqXHR;
      let aborted;
      let slot;
      let pipe;
      const options = that._getAJAXSettings(data);
      const send = function () {
        that._sending += 1;
        // Set timer for bitrate progress calculation:
        options._bitrateTimer = new that._BitrateTimer();
        jqXHR = jqXHR || (
          ((aborted || that._trigger(
            'send',
            $.Event('send', {delegatedEvent: e}),
            options,
          ) === false)
                        && that._getXHRPromise(false, options.context, aborted))
                        || that._chunkedUpload(options) || $.ajax(options)
        ).done((result, textStatus, jqXHR) => {
          that._onDone(result, textStatus, jqXHR, options);
        }).fail((jqXHR, textStatus, errorThrown) => {
          that._onFail(jqXHR, textStatus, errorThrown, options);
        }).always((jqXHRorResult, textStatus, jqXHRorError) => {
          that._onAlways(
            jqXHRorResult,
            textStatus,
            jqXHRorError,
            options,
          );
          that._sending -= 1;
          that._active -= 1;
          if (options.limitConcurrentUploads
                                && options.limitConcurrentUploads > that._sending) {
            // Start the next queued upload,
            // that has not been aborted:
            let nextSlot = that._slots.shift();
            while (nextSlot) {
              if (that._getDeferredState(nextSlot) === 'pending') {
                nextSlot.resolve();
                break;
              }
              nextSlot = that._slots.shift();
            }
          }
          if (that._active === 0) {
            // The stop callback is triggered when all uploads have
            // been completed, equivalent to the global ajaxStop event:
            that._trigger('stop');
          }
        });
        return jqXHR;
      };
      this._beforeSend(e, options);
      if (this.options.sequentialUploads
                    || (this.options.limitConcurrentUploads
                    && this.options.limitConcurrentUploads <= this._sending)) {
        if (this.options.limitConcurrentUploads > 1) {
          slot = $.Deferred();
          this._slots.push(slot);
          pipe = slot.pipe(send);
        } else {
          this._sequence = this._sequence.pipe(send, send);
          pipe = this._sequence;
        }
        // Return the piped Promise object, enhanced with an abort method,
        // which is delegated to the jqXHR object of the current upload,
        // and jqXHR callbacks mapped to the equivalent Promise methods:
        pipe.abort = function () {
          aborted = [undefined, 'abort', 'abort'];
          if (!jqXHR) {
            if (slot) {
              slot.rejectWith(options.context, aborted);
            }
            return send();
          }
          return jqXHR.abort();
        };
        return this._enhancePromise(pipe);
      }
      return send();
    },

    _onAdd(e, data) {
      const that = this;
      let result = true;
      const options = $.extend({}, this.options, data);
      const limit = options.limitMultiFileUploads;
      const paramName = this._getParamName(options);
      let paramNameSet;
      let paramNameSlice;
      let fileSet;
      let i;

      if (!(options.singleFileUploads || limit)
                    || !this._isXHRUpload(options)) {
        fileSet = [data.files];
        paramNameSet = [paramName];
      } else if (!options.singleFileUploads && limit) {
        fileSet = [];
        paramNameSet = [];
        for (i = 0; i < data.files.length; i += limit) {
          fileSet.push(data.files.slice(i, i + limit));
          paramNameSlice = paramName.slice(i, i + limit);
          if (!paramNameSlice.length) {
            paramNameSlice = paramName;
          }
          paramNameSet.push(paramNameSlice);
        }
      } else {
        paramNameSet = paramName;
      }
      data.originalFiles = data.files;
      $.each(fileSet || data.files, (index, element) => {
        const newData = $.extend({}, data);
        newData.files = fileSet ? element : [element];
        newData.paramName = paramNameSet[index];
        that._initResponseObject(newData);
        that._initProgressObject(newData);
        that._addConvenienceMethods(e, newData);
        result = that._trigger(
          'add',
          $.Event('add', {delegatedEvent: e}),
          newData,
        );
        return result;
      });
      return result;
    },

    _replaceFileInput(input) {
      const inputClone = input.clone(true);
      $('<form></form>').append(inputClone)[0].reset();
      // Detaching allows to insert the fileInput on another form
      // without loosing the file input value:
      input.after(inputClone).detach();
      // Avoid memory leaks with the detached file input:
      $.cleanData(input.unbind('remove'));
      // Replace the original file input element in the fileInput
      // elements set with the clone, which has been copied including
      // event handlers:
      this.options.fileInput = this.options.fileInput.map((i, el) => {
        if (el === input[0]) {
          return inputClone[0];
        }
        return el;
      });
      // If the widget has been initialized on the file input itself,
      // override this.element with the file input clone:
      if (input[0] === this.element[0]) {
        this.element = inputClone;
      }
    },

    _handleFileTreeEntry(entry, path) {
      const that = this;
      const dfd = $.Deferred();
      const errorHandler = function (e) {
        if (e && !e.entry) {
          e.entry = entry;
        }
        // Since $.when returns immediately if one
        // Deferred is rejected, we use resolve instead.
        // This allows valid files and invalid items
        // to be returned together in one set:
        dfd.resolve([e]);
      };
      let dirReader;
      path = path || '';
      if (entry.isFile) {
        if (entry._file) {
          // Workaround for Chrome bug #149735
          entry._file.relativePath = path;
          dfd.resolve(entry._file);
        } else {
          entry.file((file) => {
            file.relativePath = path;
            dfd.resolve(file);
          }, errorHandler);
        }
      } else if (entry.isDirectory) {
        dirReader = entry.createReader();
        dirReader.readEntries((entries) => {
          that._handleFileTreeEntries(
            entries,
            `${path + entry.name}/`,
          ).done((files) => {
            dfd.resolve(files);
          }).fail(errorHandler);
        }, errorHandler);
      } else {
        // Return an empy list for file system items
        // other than files or directories:
        dfd.resolve([]);
      }
      return dfd.promise();
    },

    _handleFileTreeEntries(entries, path) {
      const that = this;

      return $.when.apply(
        $,
        $.map(entries, (entry) => that._handleFileTreeEntry(entry, path)),
      ).pipe(function () {
        return Array.prototype.concat.apply(
          [],
          arguments,
        );
      });
    },

    _getDroppedFiles(dataTransfer) {
      dataTransfer = dataTransfer || {};
      const {items} = dataTransfer;

      if (items && items.length && (items[0].webkitGetAsEntry
                    || items[0].getAsEntry)) {
        return this._handleFileTreeEntries(
          $.map(items, (item) => {
            let entry;

            if (item.webkitGetAsEntry) {
              entry = item.webkitGetAsEntry();
              if (entry) {
                // Workaround for Chrome bug #149735:
                entry._file = item.getAsFile();
              }
              return entry;
            }
            return item.getAsEntry();
          }),
        );
      }
      return $.Deferred().resolve(
        $.makeArray(dataTransfer.files),
      ).promise();
    },

    _getSingleFileInputFiles(fileInput) {
      fileInput = $(fileInput);
      const entries = fileInput.prop('webkitEntries')
                    || fileInput.prop('entries');
      let files;
      let value;

      if (entries && entries.length) {
        return this._handleFileTreeEntries(entries);
      }
      files = $.makeArray(fileInput.prop('files'));
      if (!files.length) {
        value = fileInput.prop('value');
        if (!value) {
          return $.Deferred().resolve([]).promise();
        }
        // If the files property is not available, the browser does not
        // support the File API and we add a pseudo File object with
        // the input value as name with path information removed:
        files = [{name: value.replace(/^.*\\/, '')}];
      } else if (files[0].name === undefined && files[0].fileName) {
        // File normalization for Safari 4 and Firefox 3:
        $.each(files, (index, file) => {
          file.name = file.fileName;
          file.size = file.fileSize;
        });
      }
      return $.Deferred().resolve(files).promise();
    },

    _getFileInputFiles(fileInput) {
      if (!(fileInput instanceof $) || fileInput.length === 1) {
        return this._getSingleFileInputFiles(fileInput);
      }
      return $.when.apply(
        $,
        $.map(fileInput, this._getSingleFileInputFiles),
      ).pipe(function () {
        return Array.prototype.concat.apply(
          [],
          arguments,
        );
      });
    },

    _onChange(e) {
      const that = this;
      const data = {
        fileInput: $(e.target),
        form: $(e.target.form),
      };
      this._getFileInputFiles(data.fileInput).always((files) => {
        data.files = files;
        if (that.options.replaceFileInput) {
          that._replaceFileInput(data.fileInput);
        }
        if (that._trigger(
          'change',
          $.Event('change', {delegatedEvent: e}),
          data,
        ) !== false) {
          that._onAdd(e, data);
        }
      });
    },

    _onPaste(e) {
      const items = e.originalEvent && e.originalEvent.clipboardData
                    && e.originalEvent.clipboardData.items;
      const data = {files: []};

      if (items && items.length) {
        $.each(items, (index, item) => {
          const file = item.getAsFile && item.getAsFile();

          if (file) {
            data.files.push(file);
          }
        });
        if (this._trigger(
          'paste',
          $.Event('paste', {delegatedEvent: e}),
          data,
        ) !== false) {
          this._onAdd(e, data);
        }
      }
    },

    _onDrop(e) {
      e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
      const that = this;
      const {dataTransfer} = e;
      const data = {};

      if (dataTransfer && dataTransfer.files && dataTransfer.files.length) {
        e.preventDefault();
        this._getDroppedFiles(dataTransfer).always((files) => {
          data.files = files;
          if (that._trigger(
            'drop',
            $.Event('drop', {delegatedEvent: e}),
            data,
          ) !== false) {
            that._onAdd(e, data);
          }
        });
      }
    },

    _onDragOver(e) {
      e.dataTransfer = e.originalEvent && e.originalEvent.dataTransfer;
      const {dataTransfer} = e;

      if (dataTransfer && $.inArray('Files', dataTransfer.types) !== -1
                    && this._trigger(
                      'dragover',
                      $.Event('dragover', {delegatedEvent: e}),
                    ) !== false) {
        e.preventDefault();
        dataTransfer.dropEffect = 'copy';
      }
    },

    _initEventHandlers() {
      if (this._isXHRUpload(this.options)) {
        this._on(this.options.dropZone, {
          dragover: this._onDragOver,
          drop: this._onDrop,
        });
        this._on(this.options.pasteZone, {
          paste: this._onPaste,
        });
      }
      if ($.support.fileInput) {
        this._on(this.options.fileInput, {
          change: this._onChange,
        });
      }
    },

    _destroyEventHandlers() {
      this._off(this.options.dropZone, 'dragover drop');
      this._off(this.options.pasteZone, 'paste');
      this._off(this.options.fileInput, 'change');
    },

    _setOption(key, value) {
      const reinit = $.inArray(key, this._specialOptions) !== -1;

      if (reinit) {
        this._destroyEventHandlers();
      }
      this._super(key, value);
      if (reinit) {
        this._initSpecialOptions();
        this._initEventHandlers();
      }
    },

    _initSpecialOptions() {
      const {options} = this;

      if (options.fileInput === undefined) {
        options.fileInput = this.element.is('input[type="file"]')
          ? this.element : this.element.find('input[type="file"]');
      } else if (!(options.fileInput instanceof $)) {
        options.fileInput = $(options.fileInput);
      }
      if (!(options.dropZone instanceof $)) {
        options.dropZone = $(options.dropZone);
      }
      if (!(options.pasteZone instanceof $)) {
        options.pasteZone = $(options.pasteZone);
      }
    },

    _getRegExp(str) {
      const parts = str.split('/');
      const modifiers = parts.pop();
      parts.shift();
      return new RegExp(parts.join('/'), modifiers);
    },

    _isRegExpOption(key, value) {
      return key !== 'url' && $.type(value) === 'string'
                && /^\/.*\/[igm]{0,3}$/.test(value);
    },

    _initDataAttributes() {
      const that = this;
      const {options} = this;
      // Initialize options set via HTML5 data-attributes:
      $.each(
        $(this.element[0].cloneNode(false)).data(),
        (key, value) => {
          if (that._isRegExpOption(key, value)) {
            value = that._getRegExp(value);
          }
          options[key] = value;
        },
      );
    },

    _create() {
      this._initDataAttributes();
      this._initSpecialOptions();
      this._slots = [];
      this._sequence = this._getXHRPromise(true);
      this._sending = this._active = 0;
      this._initProgressObject(this);
      this._initEventHandlers();
    },

    // This method is exposed to the widget API and allows to query
    // the number of active uploads:
    active() {
      return this._active;
    },

    // This method is exposed to the widget API and allows to query
    // the widget upload progress.
    // It returns an object with loaded, total and bitrate properties
    // for the running uploads:
    progress() {
      return this._progress;
    },

    // This method is exposed to the widget API and allows adding files
    // using the fileupload API. The data parameter accepts an object which
    // must have a files property and can contain additional options:
    // .fileupload('add', {files: filesList});
    add(data) {
      const that = this;

      if (!data || this.options.disabled) {
        return;
      }
      if (data.fileInput && !data.files) {
        this._getFileInputFiles(data.fileInput).always((files) => {
          data.files = files;
          that._onAdd(null, data);
        });
      } else {
        data.files = $.makeArray(data.files);
        this._onAdd(null, data);
      }
    },

    // This method is exposed to the widget API and allows sending files
    // using the fileupload API. The data parameter accepts an object which
    // must have a files or fileInput property and can contain additional options:
    // .fileupload('send', {files: filesList});
    // The method returns a Promise object for the file upload call.
    send(data) {
      if (data && !this.options.disabled) {
        if (data.fileInput && !data.files) {
          const that = this;
          const dfd = $.Deferred();
          const promise = dfd.promise();
          let jqXHR;
          let aborted;
          promise.abort = function () {
            aborted = true;
            if (jqXHR) {
              return jqXHR.abort();
            }
            dfd.reject(null, 'abort', 'abort');
            return promise;
          };
          this._getFileInputFiles(data.fileInput).always(
            (files) => {
              if (aborted) {
                return;
              }
              if (!files.length) {
                dfd.reject();
                return;
              }
              data.files = files;
              jqXHR = that._onSend(null, data).then(
                (result, textStatus, jqXHR) => {
                  dfd.resolve(result, textStatus, jqXHR);
                },
                (jqXHR, textStatus, errorThrown) => {
                  dfd.reject(jqXHR, textStatus, errorThrown);
                },
              );
            },
          );
          return this._enhancePromise(promise);
        }
        data.files = $.makeArray(data.files);
        if (data.files.length) {
          return this._onSend(null, data);
        }
      }
      return this._getXHRPromise(false, data && data.context);
    },

  });
}));
