/* eslint-disable no-undef */
// @ts-nocheck
/*
 * MIT Licensed
 * https://www.twentythree.com
 * https://github.com/23/resumable.js
 * Steffen Fagerström Christensen, steffen@twentythree.com
 */

;(function() {
  'use strict'

  var Resumable = function(opts) {
    if (!(this instanceof Resumable)) {
      return new Resumable(opts)
    }
    this.version = 1.0
    // SUPPORTED BY BROWSER?
    // Check if these features are support by the browser:
    // - File object type
    // - Blob object type
    // - FileList object type
    // - slicing files
    this.support =
      typeof File !== 'undefined' &&
      typeof Blob !== 'undefined' &&
      typeof FileList !== 'undefined' &&
      (!!Blob.prototype.webkitSlice || !!Blob.prototype.mozSlice || !!Blob.prototype.slice || false)
    if (!this.support) return false

    // PROPERTIES
    var $ = this
    $.files = []
    $.defaults = {
      chunkSize: 1 * 1024 * 1024,
      forceChunkSize: false,
      simultaneousUploads: 3,
      fileParameterName: 'file',
      chunkNumberParameterName: 'resumableChunkNumber',
      chunkSizeParameterName: 'resumableChunkSize',
      currentChunkSizeParameterName: 'resumableCurrentChunkSize',
      totalSizeParameterName: 'resumableTotalSize',
      typeParameterName: 'resumableType',
      identifierParameterName: 'resumableIdentifier',
      fileNameParameterName: 'resumableFilename',
      relativePathParameterName: 'resumableRelativePath',
      totalChunksParameterName: 'resumableTotalChunks',
      dragOverClass: 'dragover',
      throttleProgressCallbacks: 0.5,
      query: {},
      headers: {},
      preprocess: null,
      preprocessFile: null,
      method: 'multipart',
      uploadMethod: 'POST',
      testMethod: 'GET',
      prioritizeFirstAndLastChunk: false,
      target: '/',
      testTarget: null,
      parameterNamespace: '',
      testChunks: true,
      generateUniqueIdentifier: null,
      getTarget: null,
      maxChunkRetries: 100,
      chunkRetryInterval: undefined,
      permanentErrors: [400, 401, 403, 404, 409, 415, 500, 501],
      maxFiles: undefined,
      withCredentials: false,
      xhrTimeout: 0,
      clearInput: true,
      chunkFormat: 'blob',
      setChunkTypeFromFile: false,
      maxFilesErrorCallback: function(files, errorCount) {
        var maxFiles = $.getOpt('maxFiles')
        alert(
          'Please upload no more than ' +
            maxFiles +
            ' file' +
            (maxFiles === 1 ? '' : 's') +
            ' at a time.'
        )
      },
      minFileSize: 1,
      minFileSizeErrorCallback: function(file, errorCount) {
        alert(
          file.fileName ||
            file.name +
              ' is too small, please upload files larger than ' +
              $h.formatSize($.getOpt('minFileSize')) +
              '.'
        )
      },
      maxFileSize: undefined,
      maxFileSizeErrorCallback: function(file, errorCount) {
        alert(
          file.fileName ||
            file.name +
              ' is too large, please upload files less than ' +
              $h.formatSize($.getOpt('maxFileSize')) +
              '.'
        )
      },
      fileType: [],
      fileTypeErrorCallback: function(file, errorCount) {
        alert(
          file.fileName ||
            file.name +
              ' has type not allowed, please upload files of type ' +
              $.getOpt('fileType') +
              '.'
        )
      },
    }
    $.opts = opts || {}
    $.getOpt = function(o) {
      var $opt = this
      // Get multiple option if passed an array
      if (o instanceof Array) {
        var options = {}
        $h.each(o, function(option) {
          options[option] = $opt.getOpt(option)
        })
        return options
      }
      // Otherwise, just return a simple option
      if ($opt instanceof ResumableChunk) {
        if (typeof $opt.opts[o] !== 'undefined') {
          return $opt.opts[o]
        } else {
          $opt = $opt.fileObj
        }
      }
      if ($opt instanceof ResumableFile) {
        if (typeof $opt.opts[o] !== 'undefined') {
          return $opt.opts[o]
        } else {
          $opt = $opt.resumableObj
        }
      }
      if ($opt instanceof Resumable) {
        if (typeof $opt.opts[o] !== 'undefined') {
          return $opt.opts[o]
        } else {
          return $opt.defaults[o]
        }
      }
    }
    $.indexOf = function(array, obj) {
      if (array.indexOf) {
        return array.indexOf(obj)
      }
      for (var i = 0; i < array.length; i++) {
        if (array[i] === obj) {
          return i
        }
      }
      return -1
    }

    // EVENTS
    // catchAll(event, ...)
    // fileSuccess(file), fileProgress(file), fileAdded(file, event), filesAdded(files, filesSkipped), fileRetry(file),
    // fileError(file, message), complete(), progress(), error(message, file), pause()
    $.events = []
    $.on = function(event, callback) {
      $.events.push(event.toLowerCase(), callback)
    }
    $.fire = function() {
      // `arguments` is an object, not array, in FF, so:
      var args = []
      for (let i = 0; i < arguments.length; i++) args.push(arguments[i])
      // Find event listeners, and support pseudo-event `catchAll`
      var event = args[0].toLowerCase()
      for (var i = 0; i <= $.events.length; i += 2) {
        if ($.events[i] === event) $.events[i + 1].apply($, args.slice(1))
        if ($.events[i] === 'catchall') $.events[i + 1].apply(null, args)
      }
      if (event === 'fileerror') $.fire('error', args[2], args[1])
      if (event === 'fileprogress') $.fire('progress')
    }

    // INTERNAL HELPER METHODS (handy, but ultimately not part of uploading)
    var $h = {
      stopEvent: function(e) {
        e.stopPropagation()
        e.preventDefault()
      },
      each: function(o, callback) {
        if (typeof o.length !== 'undefined') {
          for (var i = 0; i < o.length; i++) {
            // Array or FileList
            if (callback(o[i]) === false) return
          }
        } else {
          for (const i in o) {
            // Object
            if (callback(i, o[i]) === false) return
          }
        }
      },
      generateUniqueIdentifier: function(file, event) {
        var custom = $.getOpt('generateUniqueIdentifier')
        if (typeof custom === 'function') {
          return custom(file, event)
        }
        var relativePath =
          file.webkitRelativePath || file.relativePath || file.fileName || file.name // Some confusion in different versions of Firefox
        var size = file.size
        return size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/gim, '')
      },
      contains: function(array, test) {
        var result = false

        $h.each(array, function(value) {
          if (value === test) {
            result = true
            return false
          }
          return true
        })

        return result
      },
      formatSize: function(size) {
        if (size < 1024) {
          return size + ' bytes'
        } else if (size < 1024 * 1024) {
          return (size / 1024.0).toFixed(0) + ' KB'
        } else if (size < 1024 * 1024 * 1024) {
          return (size / 1024.0 / 1024.0).toFixed(1) + ' MB'
        } else {
          return (size / 1024.0 / 1024.0 / 1024.0).toFixed(1) + ' GB'
        }
      },
      getTarget: function(request, params) {
        var target = $.getOpt('target')

        if (request === 'test' && $.getOpt('testTarget')) {
          target = $.getOpt('testTarget') === '/' ? $.getOpt('target') : $.getOpt('testTarget')
        }

        if (typeof target === 'function') {
          return target(params)
        }

        var separator = target.indexOf('?') < 0 ? '?' : '&'
        var joinedParams = params.join('&')

        if (joinedParams) target = target + separator + joinedParams

        return target
      },
    }

    var onDrop = function(e) {
      e.currentTarget.classList.remove($.getOpt('dragOverClass'))
      $h.stopEvent(e)

      // handle dropped things as items if we can (this lets us deal with folders nicer in some cases)
      if (e.dataTransfer && e.dataTransfer.items) {
        loadFiles(e.dataTransfer.items, e)
      }
      // else handle them as files
      else if (e.dataTransfer && e.dataTransfer.files) {
        loadFiles(e.dataTransfer.files, e)
      }
    }
    var onDragLeave = function(e) {
      e.currentTarget.classList.remove($.getOpt('dragOverClass'))
    }
    var onDragOverEnter = function(e) {
      e.preventDefault()
      var dt = e.dataTransfer
      if ($.indexOf(dt.types, 'Files') >= 0) {
        // only for file drop
        e.stopPropagation()
        dt.dropEffect = 'copy'
        dt.effectAllowed = 'copy'
        e.currentTarget.classList.add($.getOpt('dragOverClass'))
      } else {
        // not work on IE/Edge....
        dt.dropEffect = 'none'
        dt.effectAllowed = 'none'
      }
    }

    /**
     * processes a single upload item (file or directory)
     * @param {Object} item item to upload, may be file or directory entry
     * @param {string} path current file path
     * @param {File[]} items list of files to append new items to
     * @param {Function} cb callback invoked when item is processed
     */
    function processItem(item, path, items, cb) {
      var entry
      if (item.isFile) {
        // file provided
        return item.file(function(file) {
          file.relativePath = path + file.name
          items.push(file)
          cb()
        })
      } else if (item.isDirectory) {
        // item is already a directory entry, just assign
        entry = item
      } else if (item instanceof File) {
        items.push(item)
      }
      if (typeof item.webkitGetAsEntry === 'function') {
        // get entry from file object
        entry = item.webkitGetAsEntry()
      }
      if (entry && entry.isDirectory) {
        // directory provided, process it
        return processDirectory(entry, path + entry.name + '/', items, cb)
      }
      if (typeof item.getAsFile === 'function') {
        // item represents a File object, convert it
        item = item.getAsFile()
        if (item instanceof File) {
          item.relativePath = path + item.name
          items.push(item)
        }
      }
      cb() // indicate processing is done
    }

    /**
     * cps-style list iteration.
     * invokes all functions in list and waits for their callback to be
     * triggered.
     * @param  {Function[]}   items list of functions expecting callback parameter
     * @param  {Function} cb    callback to trigger after the last callback has been invoked
     */
    function processCallbacks(items, cb) {
      if (!items || items.length === 0) {
        // empty or no list, invoke callback
        return cb()
      }
      // invoke current function, pass the next part as continuation
      items[0](function() {
        processCallbacks(items.slice(1), cb)
      })
    }

    /**
     * recursively traverse directory and collect files to upload
     * @param  {Object}   directory directory to process
     * @param  {string}   path      current path
     * @param  {File[]}   items     target list of items
     * @param  {Function} cb        callback invoked after traversing directory
     */
    function processDirectory(directory, path, items, cb) {
      var dirReader = directory.createReader()
      var allEntries = []

      function readEntries() {
        dirReader.readEntries(function(entries) {
          if (entries.length) {
            allEntries = allEntries.concat(entries)
            return readEntries()
          }

          // process all conversion callbacks, finally invoke own one
          processCallbacks(
            allEntries.map(function(entry) {
              // bind all properties except for callback
              return processItem.bind(null, entry, path, items)
            }),
            cb
          )
        })
      }

      readEntries()
    }

    /**
     * process items to extract files to be uploaded
     * @param  {File[]} items items to process
     * @param  {Event} event event that led to upload
     */
    function loadFiles(items, event) {
      if (!items.length) {
        return // nothing to do
      }
      $.fire('beforeAdd')
      var files = []
      processCallbacks(
        Array.prototype.map.call(items, function(item) {
          // bind all properties except for callback
          var entry = item
          if (typeof item.webkitGetAsEntry === 'function') {
            entry = item.webkitGetAsEntry()
          }
          return processItem.bind(null, entry, '', files)
        }),
        function() {
          if (files.length) {
            // at least one file found
            appendFilesFromFileList(files, event)
          }
        }
      )
    }

    var appendFilesFromFileList = function(fileList, event) {
      // check for uploading too many files
      var errorCount = 0
      var o = $.getOpt([
        'maxFiles',
        'minFileSize',
        'maxFileSize',
        'maxFilesErrorCallback',
        'minFileSizeErrorCallback',
        'maxFileSizeErrorCallback',
        'fileType',
        'fileTypeErrorCallback',
      ])
      if (typeof o.maxFiles !== 'undefined' && o.maxFiles < fileList.length + $.files.length) {
        // if single-file upload, file is already added, and trying to add 1 new file, simply replace the already-added file
        if (o.maxFiles === 1 && $.files.length === 1 && fileList.length === 1) {
          $.removeFile($.files[0])
        } else {
          o.maxFilesErrorCallback(fileList, errorCount++)
          return false
        }
      }
      var files = []
      var filesSkipped = []
      var remaining = fileList.length
      var decreaseReamining = function() {
        if (!--remaining) {
          // all files processed, trigger event
          if (!files.length && !filesSkipped.length) {
            // no succeeded files, just skip
            return
          }
          window.setTimeout(function() {
            $.fire('filesAdded', files, filesSkipped)
          }, 0)
        }
      }
      $h.each(fileList, function(file) {
        var fileName = file.name
        var fileType = file.type // e.g video/mp4
        if (o.fileType.length > 0) {
          var fileTypeFound = false
          for (var index in o.fileType) {
            // For good behaviour we do some inital sanitizing. Remove spaces and lowercase all
            o.fileType[index] = o.fileType[index].replace(/\s/g, '').toLowerCase()

            // Allowing for both [extension, .extension, mime/type, mime/*]
            var extension = (o.fileType[index].match(/^[^.][^/]+$/) ? '.' : '') + o.fileType[index]

            if (
              fileName.substr(-1 * extension.length).toLowerCase() === extension ||
              // If MIME type, check for wildcard or if extension matches the files tiletype
              (extension.indexOf('/') !== -1 &&
                ((extension.indexOf('*') !== -1 &&
                  fileType.substr(0, extension.indexOf('*')) ===
                    extension.substr(0, extension.indexOf('*'))) ||
                  fileType === extension))
            ) {
              fileTypeFound = true
              break
            }
          }
          if (!fileTypeFound) {
            o.fileTypeErrorCallback(file, errorCount++)
            return true
          }
        }

        if (typeof o.minFileSize !== 'undefined' && file.size < o.minFileSize) {
          o.minFileSizeErrorCallback(file, errorCount++)
          return true
        }
        if (typeof o.maxFileSize !== 'undefined' && file.size > o.maxFileSize) {
          o.maxFileSizeErrorCallback(file, errorCount++)
          return true
        }

        function addFile(uniqueIdentifier) {
          if (!$.getFromUniqueIdentifier(uniqueIdentifier)) {
            ;(function() {
              file.uniqueIdentifier = uniqueIdentifier
              var f = new ResumableFile($, file, uniqueIdentifier)
              $.files.push(f)
              files.push(f)
              f.container = typeof event !== 'undefined' ? event.srcElement : null
              window.setTimeout(function() {
                $.fire('fileAdded', f, event)
              }, 0)
            })()
          } else {
            filesSkipped.push(file)
          }
          decreaseReamining()
        }
        // directories have size === 0
        var uniqueIdentifier = $h.generateUniqueIdentifier(file, event)
        if (uniqueIdentifier && typeof uniqueIdentifier.then === 'function') {
          // Promise or Promise-like object provided as unique identifier
          uniqueIdentifier.then(
            function(uniqueIdentifier) {
              // unique identifier generation succeeded
              addFile(uniqueIdentifier)
            },
            function() {
              // unique identifier generation failed
              // skip further processing, only decrease file count
              decreaseReamining()
            }
          )
        } else {
          // non-Promise provided as unique identifier, process synchronously
          addFile(uniqueIdentifier)
        }
      })
    }

    // INTERNAL OBJECT TYPES
    function ResumableFile(resumableObj, file, uniqueIdentifier) {
      var $ = this
      $.opts = {}
      $.getOpt = resumableObj.getOpt
      $._prevProgress = 0
      $.resumableObj = resumableObj
      $.file = file
      $.fileName = file.fileName || file.name // Some confusion in different versions of Firefox
      $.size = file.size
      $.relativePath = file.relativePath || file.webkitRelativePath || $.fileName
      $.uniqueIdentifier = uniqueIdentifier
      $._pause = false
      $.container = ''
      $.preprocessState = 0 // 0 = unprocessed, 1 = processing, 2 = finished
      var _error = uniqueIdentifier !== undefined

      // Callback when something happens within the chunk
      var chunkEvent = function(event, message) {
        // event can be 'progress', 'success', 'error' or 'retry'
        switch (event) {
          case 'progress':
            $.resumableObj.fire('fileProgress', $, message)
            break
          case 'error':
            $.abort()
            _error = true
            $.chunks = []
            $.resumableObj.fire('fileError', $, message)
            break
          case 'success':
            if (_error) return
            $.resumableObj.fire('fileProgress', $, message) // it's at least progress
            if ($.isComplete()) {
              $.resumableObj.fire('fileSuccess', $, message)
            }
            break
          case 'retry':
            $.resumableObj.fire('fileRetry', $)
            break
        }
      }

      // Main code to set up a file object with chunks,
      // packaged to be able to handle retries if needed.
      $.chunks = []
      $.abort = function() {
        // Stop current uploads
        var abortCount = 0
        $h.each($.chunks, function(c) {
          if (c.status() === 'uploading') {
            c.abort()
            abortCount++
          }
        })
        if (abortCount > 0) $.resumableObj.fire('fileProgress', $)
      }
      $.cancel = function() {
        // Reset this file to be void
        var _chunks = $.chunks
        $.chunks = []
        // Stop current uploads
        $h.each(_chunks, function(c) {
          if (c.status() === 'uploading') {
            c.abort()
            $.resumableObj.uploadNextChunk()
          }
        })
        $.resumableObj.removeFile($)
        $.resumableObj.fire('fileProgress', $)
      }
      $.retry = function() {
        $.bootstrap()
        var firedRetry = false
        $.resumableObj.on('chunkingComplete', function() {
          if (!firedRetry) $.resumableObj.upload()
          firedRetry = true
        })
      }
      $.bootstrap = function() {
        $.abort()
        _error = false
        // Rebuild stack of chunks from file
        $.chunks = []
        $._prevProgress = 0
        var round = $.getOpt('forceChunkSize') ? Math.ceil : Math.floor
        var maxOffset = Math.max(round($.file.size / $.getOpt('chunkSize')), 1)
        for (var offset = 0; offset < maxOffset; offset++) {
          ;(function(offset) {
            $.chunks.push(new ResumableChunk($.resumableObj, $, offset, chunkEvent))
            $.resumableObj.fire('chunkingProgress', $, offset / maxOffset)
          })(offset)
        }
        window.setTimeout(function() {
          $.resumableObj.fire('chunkingComplete', $)
        }, 0)
      }
      $.progress = function() {
        if (_error) return 1
        // Sum up progress across everything
        var ret = 0
        var error = false
        $h.each($.chunks, function(c) {
          if (c.status() === 'error') error = true
          ret += c.progress(true) // get chunk progress relative to entire file
        })
        ret = error ? 1 : ret > 0.99999 ? 1 : ret
        ret = Math.max($._prevProgress, ret) // We don't want to lose percentages when an upload is paused
        $._prevProgress = ret
        return ret
      }
      $.isUploading = function() {
        var uploading = false
        $h.each($.chunks, function(chunk) {
          if (chunk.status() === 'uploading') {
            uploading = true
            return false
          }
        })
        return uploading
      }
      $.isComplete = function() {
        var outstanding = false
        if ($.preprocessState === 1) {
          return false
        }
        $h.each($.chunks, function(chunk) {
          var status = chunk.status()
          if (status === 'pending' || status === 'uploading' || chunk.preprocessState === 1) {
            outstanding = true
            return false
          }
        })
        return !outstanding
      }
      $.pause = function(pause) {
        if (typeof pause === 'undefined') {
          $._pause = !$._pause
        } else {
          $._pause = pause
        }
      }
      $.isPaused = function() {
        return $._pause
      }
      $.preprocessFinished = function() {
        $.preprocessState = 2
        $.upload()
      }
      $.upload = function() {
        var found = false
        if ($.isPaused() === false) {
          var preprocess = $.getOpt('preprocessFile')
          if (typeof preprocess === 'function') {
            switch ($.preprocessState) {
              case 0:
                $.preprocessState = 1
                preprocess($)
                return true
              case 1:
                return true
              case 2:
                break
            }
          }
          $h.each($.chunks, function(chunk) {
            if (chunk.status() === 'pending' && chunk.preprocessState !== 1) {
              chunk.send()
              found = true
              return false
            }
          })
        }
        return found
      }
      $.markChunksCompleted = function(chunkNumber) {
        if (!$.chunks || $.chunks.length <= chunkNumber) {
          return
        }
        for (var num = 0; num < chunkNumber; num++) {
          $.chunks[num].markComplete = true
        }
      }

      // Bootstrap and return
      $.resumableObj.fire('chunkingStart', $)
      $.bootstrap()
      return this
    }

    function ResumableChunk(resumableObj, fileObj, offset, callback) {
      var $ = this
      $.opts = {}
      $.getOpt = resumableObj.getOpt
      $.resumableObj = resumableObj
      $.fileObj = fileObj
      $.fileObjSize = fileObj.size
      $.fileObjType = fileObj.file.type
      $.offset = offset
      $.callback = callback
      $.lastProgressCallback = new Date()
      $.tested = false
      $.retries = 0
      $.pendingRetry = false
      $.preprocessState = 0 // 0 = unprocessed, 1 = processing, 2 = finished
      $.markComplete = false

      // Computed properties
      var chunkSize = $.getOpt('chunkSize')
      $.loaded = 0
      $.startByte = $.offset * chunkSize
      $.endByte = Math.min($.fileObjSize, ($.offset + 1) * chunkSize)
      if ($.fileObjSize - $.endByte < chunkSize && !$.getOpt('forceChunkSize')) {
        // The last chunk will be bigger than the chunk size, but less than 2*chunkSize
        $.endByte = $.fileObjSize
      }
      $.xhr = null

      // test() makes a GET request without any data to see if the chunk has already been uploaded in a previous session
      $.test = function() {
        // Set up request and listen for event
        $.xhr = new XMLHttpRequest()

        var testHandler = function(e) {
          $.tested = true
          var status = $.status()
          if (status === 'success') {
            $.callback(status, $.message())
            $.resumableObj.uploadNextChunk()
          } else {
            $.send()
          }
        }
        $.xhr.addEventListener('load', testHandler, false)
        $.xhr.addEventListener('error', testHandler, false)
        $.xhr.addEventListener('timeout', testHandler, false)

        // Add data from the query options
        var params = []
        var parameterNamespace = $.getOpt('parameterNamespace')
        var customQuery = $.getOpt('query')
        if (typeof customQuery === 'function') customQuery = customQuery($.fileObj, $)
        $h.each(customQuery, function(k, v) {
          params.push([encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('='))
        })
        // Add extra data to identify chunk
        params = params.concat(
          [
            // define key/value pairs for additional parameters
            ['chunkNumberParameterName', $.offset + 1],
            ['chunkSizeParameterName', $.getOpt('chunkSize')],
            ['currentChunkSizeParameterName', $.endByte - $.startByte],
            ['totalSizeParameterName', $.fileObjSize],
            ['typeParameterName', $.fileObjType],
            ['identifierParameterName', $.fileObj.uniqueIdentifier],
            ['fileNameParameterName', $.fileObj.fileName],
            ['relativePathParameterName', $.fileObj.relativePath],
            ['totalChunksParameterName', $.fileObj.chunks.length],
          ]
            .filter(function(pair) {
              // include items that resolve to truthy values
              // i.e. exclude false, null, undefined and empty strings
              return $.getOpt(pair[0])
            })
            .map(function(pair) {
              // map each key/value pair to its final form
              return [parameterNamespace + $.getOpt(pair[0]), encodeURIComponent(pair[1])].join('=')
            })
        )
        // Append the relevant chunk and send it
        $.xhr.open($.getOpt('testMethod'), $h.getTarget('test', params))
        $.xhr.timeout = $.getOpt('xhrTimeout')
        $.xhr.withCredentials = $.getOpt('withCredentials')
        // Add data from header options
        var customHeaders = $.getOpt('headers')
        if (typeof customHeaders === 'function') {
          customHeaders = customHeaders($.fileObj, $)
        }
        $h.each(customHeaders, function(k, v) {
          $.xhr.setRequestHeader(k, v)
        })
        $.xhr.send(null)
      }

      $.preprocessFinished = function() {
        $.preprocessState = 2
        $.send()
      }

      // send() uploads the actual data in a POST call
      $.send = function() {
        var preprocess = $.getOpt('preprocess')
        if (typeof preprocess === 'function') {
          switch ($.preprocessState) {
            case 0:
              $.preprocessState = 1
              preprocess($)
              return
            case 1:
              return
            case 2:
              break
          }
        }
        if ($.getOpt('testChunks') && !$.tested) {
          $.test()
          return
        }

        // Set up request and listen for event
        $.xhr = new XMLHttpRequest()

        // Progress
        $.xhr.upload.addEventListener(
          'progress',
          function(e) {
            if (
              new Date() - $.lastProgressCallback >
              $.getOpt('throttleProgressCallbacks') * 1000
            ) {
              $.callback('progress')
              $.lastProgressCallback = new Date()
            }
            $.loaded = e.loaded || 0
          },
          false
        )
        $.loaded = 0
        $.pendingRetry = false
        $.callback('progress')

        // Done (either done, failed or retry)
        var doneHandler = function(e) {
          var status = $.status()
          if (status === 'success' || status === 'error') {
            $.callback(status, $.message())
            $.resumableObj.uploadNextChunk()
          } else {
            $.callback('retry', $.message())
            $.abort()
            $.retries++
            var retryInterval = $.getOpt('chunkRetryInterval')
            if (retryInterval !== undefined) {
              $.pendingRetry = true
              setTimeout($.send, retryInterval)
            } else {
              $.send()
            }
          }
        }
        $.xhr.addEventListener('load', doneHandler, false)
        $.xhr.addEventListener('error', doneHandler, false)
        $.xhr.addEventListener('timeout', doneHandler, false)

        // Set up the basic query data from Resumable
        var query = [
          ['chunkNumberParameterName', $.offset + 1],
          ['chunkSizeParameterName', $.getOpt('chunkSize')],
          ['currentChunkSizeParameterName', $.endByte - $.startByte],
          ['totalSizeParameterName', $.fileObjSize],
          ['typeParameterName', $.fileObjType],
          ['identifierParameterName', $.fileObj.uniqueIdentifier],
          ['fileNameParameterName', $.fileObj.fileName],
          ['relativePathParameterName', $.fileObj.relativePath],
          ['totalChunksParameterName', $.fileObj.chunks.length],
        ]
          .filter(function(pair) {
            // include items that resolve to truthy values
            // i.e. exclude false, null, undefined and empty strings
            return $.getOpt(pair[0])
          })
          .reduce(function(query, pair) {
            // assign query key/value
            query[$.getOpt(pair[0])] = pair[1]
            return query
          }, {})
        // Mix in custom data
        var customQuery = $.getOpt('query')
        if (typeof customQuery === 'function') customQuery = customQuery($.fileObj, $)
        $h.each(customQuery, function(k, v) {
          query[k] = v
        })

        var func = $.fileObj.file.slice
          ? 'slice'
          : $.fileObj.file.mozSlice
          ? 'mozSlice'
          : $.fileObj.file.webkitSlice
          ? 'webkitSlice'
          : 'slice'
        var bytes = $.fileObj.file[func](
          $.startByte,
          $.endByte,
          $.getOpt('setChunkTypeFromFile') ? $.fileObj.file.type : ''
        )
        var data = null
        var params = []

        var parameterNamespace = $.getOpt('parameterNamespace')
        if ($.getOpt('method') === 'octet') {
          // Add data from the query options
          data = bytes
          $h.each(query, function(k, v) {
            params.push(
              [encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('=')
            )
          })
        } else {
          // Add data from the query options
          data = new FormData()
          $h.each(query, function(k, v) {
            data.append(parameterNamespace + k, v)
            params.push(
              [encodeURIComponent(parameterNamespace + k), encodeURIComponent(v)].join('=')
            )
          })
          if ($.getOpt('chunkFormat') === 'blob') {
            data.append(
              parameterNamespace + $.getOpt('fileParameterName'),
              bytes,
              $.fileObj.fileName
            )
          } else if ($.getOpt('chunkFormat') === 'base64') {
            var fr = new FileReader()
            fr.onload = function(e) {
              data.append(parameterNamespace + $.getOpt('fileParameterName'), fr.result)
              $.xhr.send(data)
            }
            fr.readAsDataURL(bytes)
          }
        }

        var target = $h.getTarget('upload', params)
        var method = $.getOpt('uploadMethod')

        $.xhr.open(method, target)
        if ($.getOpt('method') === 'octet') {
          $.xhr.setRequestHeader('Content-Type', 'application/octet-stream')
        }
        $.xhr.timeout = $.getOpt('xhrTimeout')
        $.xhr.withCredentials = $.getOpt('withCredentials')
        // Add data from header options
        var customHeaders = $.getOpt('headers')
        if (typeof customHeaders === 'function') {
          customHeaders = customHeaders($.fileObj, $)
        }

        $h.each(customHeaders, function(k, v) {
          $.xhr.setRequestHeader(k, v)
        })

        if ($.getOpt('chunkFormat') === 'blob') {
          $.xhr.send(data)
        }
      }
      $.abort = function() {
        // Abort and reset
        if ($.xhr) $.xhr.abort()
        $.xhr = null
      }
      $.status = function() {
        // Returns: 'pending', 'uploading', 'success', 'error'
        if ($.pendingRetry) {
          // if pending retry then that's effectively the same as actively uploading,
          // there might just be a slight delay before the retry starts
          return 'uploading'
        } else if ($.markComplete) {
          return 'success'
        } else if (!$.xhr) {
          return 'pending'
        } else if ($.xhr.readyState < 4) {
          // Status is really 'OPENED', 'HEADERS_RECEIVED' or 'LOADING' - meaning that stuff is happening
          return 'uploading'
        } else {
          if ($.xhr.status === 200 || $.xhr.status === 201) {
            // HTTP 200, 201 (created)
            return 'success'
          } else if (
            $h.contains($.getOpt('permanentErrors'), $.xhr.status) ||
            $.retries >= $.getOpt('maxChunkRetries')
          ) {
            // HTTP 400, 404, 409, 415, 500, 501 (permanent error)
            return 'error'
          } else {
            // this should never happen, but we'll reset and queue a retry
            // a likely case for this would be 503 service unavailable
            $.abort()
            return 'pending'
          }
        }
      }
      $.message = function() {
        return $.xhr ? $.xhr.responseText : ''
      }
      $.progress = function(relative) {
        if (typeof relative === 'undefined') relative = false
        var factor = relative ? ($.endByte - $.startByte) / $.fileObjSize : 1
        if ($.pendingRetry) return 0
        if ((!$.xhr || !$.xhr.status) && !$.markComplete) factor *= 0.95
        var s = $.status()
        switch (s) {
          case 'success':
          case 'error':
            return 1 * factor
          case 'pending':
            return 0 * factor
          default:
            return ($.loaded / ($.endByte - $.startByte)) * factor
        }
      }
      return this
    }

    // QUEUE
    $.uploadNextChunk = function() {
      var found = false

      // In some cases (such as videos) it's really handy to upload the first
      // and last chunk of a file quickly; this let's the server check the file's
      // metadata and determine if there's even a point in continuing.
      if ($.getOpt('prioritizeFirstAndLastChunk')) {
        $h.each($.files, function(file) {
          if (
            file.chunks.length &&
            file.chunks[0].status() === 'pending' &&
            file.chunks[0].preprocessState === 0
          ) {
            file.chunks[0].send()
            found = true
            return false
          }
          if (
            file.chunks.length > 1 &&
            file.chunks[file.chunks.length - 1].status() === 'pending' &&
            file.chunks[file.chunks.length - 1].preprocessState === 0
          ) {
            file.chunks[file.chunks.length - 1].send()
            found = true
            return false
          }
        })
        if (found) return true
      }

      // Now, simply look for the next, best thing to upload
      $h.each($.files, function(file) {
        found = file.upload()
        if (found) return false
      })
      if (found) return true

      // The are no more outstanding chunks to upload, check is everything is done
      var outstanding = false
      $h.each($.files, function(file) {
        if (!file.isComplete()) {
          outstanding = true
          return false
        }
      })
      if (!outstanding) {
        // All chunks have been uploaded, complete
        $.fire('complete')
      }
      return false
    }

    // PUBLIC METHODS FOR RESUMABLE.JS
    $.assignBrowse = function(domNodes, isDirectory) {
      if (typeof domNodes.length === 'undefined') domNodes = [domNodes]
      $h.each(domNodes, function(domNode) {
        var input
        if (domNode.tagName === 'INPUT' && domNode.type === 'file') {
          input = domNode
        } else {
          input = document.createElement('input')
          input.setAttribute('type', 'file')
          input.style.display = 'none'
          domNode.addEventListener(
            'click',
            function() {
              input.style.opacity = 0
              input.style.display = 'block'
              input.focus()
              input.click()
              input.style.display = 'none'
            },
            false
          )
          domNode.appendChild(input)
        }
        var maxFiles = $.getOpt('maxFiles')
        if (typeof maxFiles === 'undefined' || maxFiles !== 1) {
          input.setAttribute('multiple', 'multiple')
        } else {
          input.removeAttribute('multiple')
        }
        if (isDirectory) {
          input.setAttribute('webkitdirectory', 'webkitdirectory')
        } else {
          input.removeAttribute('webkitdirectory')
        }
        var fileTypes = $.getOpt('fileType')
        if (typeof fileTypes !== 'undefined' && fileTypes.length >= 1) {
          input.setAttribute(
            'accept',
            fileTypes
              .map(function(e) {
                e = e.replace(/\s/g, '').toLowerCase()
                if (e.match(/^[^.][^/]+$/)) {
                  e = '.' + e
                }
                return e
              })
              .join(',')
          )
        } else {
          input.removeAttribute('accept')
        }
        // When new files are added, simply append them to the overall list
        input.addEventListener(
          'change',
          function(e) {
            appendFilesFromFileList(e.target.files, e)
            var clearInput = $.getOpt('clearInput')
            if (clearInput) {
              e.target.value = ''
            }
          },
          false
        )
      })
    }
    $.assignDrop = function(domNodes) {
      if (typeof domNodes.length === 'undefined') domNodes = [domNodes]

      $h.each(domNodes, function(domNode) {
        domNode.addEventListener('dragover', onDragOverEnter, false)
        domNode.addEventListener('dragenter', onDragOverEnter, false)
        domNode.addEventListener('dragleave', onDragLeave, false)
        domNode.addEventListener('drop', onDrop, false)
      })
    }
    $.unAssignDrop = function(domNodes) {
      if (typeof domNodes.length === 'undefined') domNodes = [domNodes]

      $h.each(domNodes, function(domNode) {
        domNode.removeEventListener('dragover', onDragOverEnter)
        domNode.removeEventListener('dragenter', onDragOverEnter)
        domNode.removeEventListener('dragleave', onDragLeave)
        domNode.removeEventListener('drop', onDrop)
      })
    }
    $.isUploading = function() {
      var uploading = false
      $h.each($.files, function(file) {
        if (file.isUploading()) {
          uploading = true
          return false
        }
      })
      return uploading
    }
    $.upload = function() {
      // Make sure we don't start too many uploads at once
      if ($.isUploading()) return
      // Kick off the queue
      $.fire('uploadStart')
      for (var num = 1; num <= $.getOpt('simultaneousUploads'); num++) {
        $.uploadNextChunk()
      }
    }
    $.pause = function() {
      // Resume all chunks currently being uploaded
      $h.each($.files, function(file) {
        file.abort()
      })
      $.fire('pause')
    }
    $.cancel = function() {
      $.fire('beforeCancel')
      for (var i = $.files.length - 1; i >= 0; i--) {
        $.files[i].cancel()
      }
      $.fire('cancel')
    }
    $.progress = function() {
      var totalDone = 0
      var totalSize = 0
      // Resume all chunks currently being uploaded
      $h.each($.files, function(file) {
        totalDone += file.progress() * file.size
        totalSize += file.size
      })
      return totalSize > 0 ? totalDone / totalSize : 0
    }
    $.addFile = function(file, event) {
      appendFilesFromFileList([file], event)
    }
    $.addFiles = function(files, event) {
      appendFilesFromFileList(files, event)
    }
    $.removeFile = function(file) {
      for (var i = $.files.length - 1; i >= 0; i--) {
        if ($.files[i] === file) {
          $.files.splice(i, 1)
        }
      }
    }
    $.getFromUniqueIdentifier = function(uniqueIdentifier) {
      var ret = false
      $h.each($.files, function(f) {
        if (f.uniqueIdentifier === uniqueIdentifier) ret = f
      })
      return ret
    }
    $.getSize = function() {
      var totalSize = 0
      $h.each($.files, function(file) {
        totalSize += file.size
      })
      return totalSize
    }
    $.handleDropEvent = function(e) {
      onDrop(e)
    }
    $.handleChangeEvent = function(e) {
      appendFilesFromFileList(e.target.files, e)
      e.target.value = ''
    }
    $.updateQuery = function(query) {
      $.opts.query = query
    }

    return this
  }

  // Node.js-style export for Node and Component
  if (typeof module !== 'undefined') {
    // left here for backwards compatibility
    module.exports = Resumable
    module.exports.Resumable = Resumable
  } else if (typeof define === 'function' && define.amd) {
    // AMD/requirejs: Define the module
    define(function() {
      return Resumable
    })
  } else {
    // Browser: Expose to window
    window.Resumable = Resumable
  }
})()
