text: var utils = require('./utils')
  var event = require('./event')
  var File = require('./file')
  var Chunk = require('./chunk')
  
  var version = '__VERSION__'
  
  var isServer = typeof window === 'undefined'
  
  // ie10+
  var ie10plus = isServer ? false : window.navigator.msPointerEnabled
  var support = (function () {
    if (isServer) {
      return false
    }
    var sliceName = 'slice'
    var _support = utils.isDefined(window.File) && utils.isDefined(window.Blob) &&
                  utils.isDefined(window.FileList)
    var bproto = null
    if (_support) {
      bproto = window.Blob.prototype
      utils.each(['slice', 'webkitSlice', 'mozSlice'], function (n) {
        if (bproto[n]) {
          sliceName = n
          return false
        }
      })
      _support = !!bproto[sliceName]
    }
    if (_support) Uploader.sliceName = sliceName
    bproto = null
    return _support
  })()
  
  var supportDirectory = (function () {
    if (isServer) {
      return false
    }
    var input = window.document.createElement('input')
    input.type = 'file'
    var sd = 'webkitdirectory' in input || 'directory' in input
    input = null
    return sd
  })()
  
  function Uploader (opts) {
    this.support = support
    /* istanbul ignore if */
    if (!this.support) {
      return
    }
    this.supportDirectory = supportDirectory
    utils.defineNonEnumerable(this, 'filePaths', {})
    this.opts = utils.extend({}, Uploader.defaults, opts || {})
  
    this.preventEvent = utils.bind(this._preventEvent, this)
  
    File.call(this, this)
  }
  
  /**
   * Default read function using the webAPI
   *
   * @function webAPIFileRead(fileObj, fileType, startByte, endByte, chunk)
   *
   */
  var webAPIFileRead = function (fileObj, fileType, startByte, endByte, chunk) {
    chunk.readFinished(fileObj.file[Uploader.sliceName](startByte, endByte, fileType))
  }
  
  Uploader.version = version
  
  Uploader.defaults = {
    chunkSize: 1024 * 1024,
    forceChunkSize: false,
    simultaneousUploads: 3,
    singleFile: false,
    fileParameterName: 'file',
    progressCallbacksInterval: 500,
    speedSmoothingFactor: 0.1,
    query: {},
    headers: {},
    withCredentials: false,
    preprocess: null,
    method: 'multipart',
    testMethod: 'GET',
    uploadMethod: 'POST',
    prioritizeFirstAndLastChunk: false,
    allowDuplicateUploads: false,
    target: '/',
    testChunks: true,
    generateUniqueIdentifier: null,
    maxChunkRetries: 0,
    chunkRetryInterval: null,
    permanentErrors: [404, 415, 500, 501],
    successStatuses: [200, 201, 202],
    onDropStopPropagation: false,
    initFileFn: null,
    readFileFn: webAPIFileRead,
    checkChunkUploadedByResponse: null,
    initialPaused: false,
    processResponse: function (response, cb) {
      cb(null, response)
    },
    processParams: function (params) {
      return params
    }
  }
  
  Uploader.utils = utils
  Uploader.event = event
  Uploader.File = File
  Uploader.Chunk = Chunk
  
  // inherit file
  Uploader.prototype = utils.extend({}, File.prototype)
  // inherit event
  utils.extend(Uploader.prototype, event)
  utils.extend(Uploader.prototype, {
  
    constructor: Uploader,
  
    _trigger: function (name) {
      var args = utils.toArray(arguments)
      var preventDefault = !this.trigger.apply(this, arguments)
      if (name !== 'catchAll') {
        args.unshift('catchAll')
        preventDefault = !this.trigger.apply(this, args) || preventDefault
      }
      return !preventDefault
    },
  
    _triggerAsync: function () {
      var args = arguments
      utils.nextTick(function () {
        this._trigger.apply(this, args)
      }, this)
    },
  
    addFiles: function (files, evt) {
      var _files = []
      var oldFileListLen = this.fileList.length
      utils.each(files, function (file) {
        // Uploading empty file IE10/IE11 hangs indefinitely
        // Directories have size `0` and name `.`
        // Ignore already added files if opts.allowDuplicateUploads is set to false
        if ((!ie10plus || ie10plus && file.size > 0) && !(file.size % 4096 === 0 && (file.name === '.' || file.fileName === '.'))) {
          var uniqueIdentifier = this.generateUniqueIdentifier(file)
          if (this.opts.allowDuplicateUploads || !this.getFromUniqueIdentifier(uniqueIdentifier)) {
            var _file = new File(this, file, this)
            _file.uniqueIdentifier = uniqueIdentifier
            if (this._trigger('fileAdded', _file, evt)) {
              _files.push(_file)
            } else {
              File.prototype.removeFile.call(this, _file)
            }
          }
        }
      }, this)
      // get new fileList
      var newFileList = this.fileList.slice(oldFileListLen)
      if (this._trigger('filesAdded', _files, newFileList, evt)) {
        utils.each(_files, function (file) {
          if (this.opts.singleFile && this.files.length > 0) {
            this.removeFile(this.files[0])
          }
          this.files.push(file)
        }, this)
        this._trigger('filesSubmitted', _files, newFileList, evt)
      } else {
        utils.each(newFileList, function (file) {
          File.prototype.removeFile.call(this, file)
        }, this)
      }
    },
  
    addFile: function (file, evt) {
      this.addFiles([file], evt)
    },
  
    cancel: function () {
      for (var i = this.fileList.length - 1; i >= 0; i--) {
        this.fileList[i].cancel()
      }
    },
  
    removeFile: function (file) {
      File.prototype.removeFile.call(this, file)
      this._trigger('fileRemoved', file)
    },
  
    generateUniqueIdentifier: function (file) {
      var custom = this.opts.generateUniqueIdentifier
      if (utils.isFunction(custom)) {
        return custom(file)
      }
      /* istanbul ignore next */
      // Some confusion in different versions of Firefox
      var relativePath = file.relativePath || file.webkitRelativePath || file.fileName || file.name
      /* istanbul ignore next */
      return file.size + '-' + relativePath.replace(/[^0-9a-zA-Z_-]/img, '')
    },
  
    getFromUniqueIdentifier: function (uniqueIdentifier) {
      var ret = false
      utils.each(this.files, function (file) {
        if (file.uniqueIdentifier === uniqueIdentifier) {
          ret = file
          return false
        }
      })
      return ret
    },
  
    uploadNextChunk: function (preventEvents) {
      var found = false
      var pendingStatus = Chunk.STATUS.PENDING
      var checkChunkUploaded = this.uploader.opts.checkChunkUploadedByResponse
      if (this.opts.prioritizeFirstAndLastChunk) {
        utils.each(this.files, function (file) {
          if (file.paused) {
            return
          }
          if (checkChunkUploaded && !file._firstResponse && file.isUploading()) {
            // waiting for current file's first chunk response
            return
          }
          if (file.chunks.length && file.chunks[0].status() === pendingStatus) {
            file.chunks[0].send()
            found = true
            return false
          }
          if (file.chunks.length > 1 && file.chunks[file.chunks.length - 1].status() === pendingStatus) {
            file.chunks[file.chunks.length - 1].send()
            found = true
            return false
          }
        })
        if (found) {
          return found
        }
      }
  
      // Now, simply look for the next, best thing to upload
      utils.each(this.files, function (file) {
        if (!file.paused) {
          if (checkChunkUploaded && !file._firstResponse && file.isUploading()) {
            // waiting for current file's first chunk response
            return
          }
          utils.each(file.chunks, function (chunk) {
            if (chunk.status() === pendingStatus) {
              chunk.send()
              found = true
              return false
            }
          })
        }
        if (found) {
          return false
        }
      })
      if (found) {
        return true
      }
  
      // The are no more outstanding chunks to upload, check is everything is done
      var outstanding = false
      utils.each(this.files, function (file) {
        if (!file.isComplete()) {
          outstanding = true
          return false
        }
      })
      // should check files now
      // if now files in list
      // should not trigger complete event
      if (!outstanding && !preventEvents && this.files.length) {
        // All chunks have been uploaded, complete
        this._triggerAsync('complete')
      }
      return outstanding
    },
  
    upload: function (preventEvents) {
      // Make sure we don't start too many uploads at once
      var ret = this._shouldUploadNext()
      if (ret === false) {
        return
      }
      !preventEvents && this._trigger('uploadStart')
      var started = false
      for (var num = 1; num <= this.opts.simultaneousUploads - ret; num++) {
        started = this.uploadNextChunk(!preventEvents) || started
        if (!started && preventEvents) {
          // completed
          break
        }
      }
      if (!started && !preventEvents) {
        this._triggerAsync('complete')
      }
    },
  
    /**
     * should upload next chunk
     * @function
     * @returns {Boolean|Number}
     */
    _shouldUploadNext: function () {
      var num = 0
      var should = true
      var simultaneousUploads = this.opts.simultaneousUploads
      var uploadingStatus = Chunk.STATUS.UPLOADING
      utils.each(this.files, function (file) {
        utils.each(file.chunks, function (chunk) {
          if (chunk.status() === uploadingStatus) {
            num++
            if (num >= simultaneousUploads) {
              should = false
              return false
            }
          }
        })
        return should
      })
      // if should is true then return uploading chunks's length
      return should && num
    },
  
    /**
     * Assign a browse action to one or more DOM nodes.
     * @function
     * @param {Element|Array.<Element>} domNodes
     * @param {boolean} isDirectory Pass in true to allow directories to
     * @param {boolean} singleFile prevent multi file upload
     * @param {Object} attributes set custom attributes:
     *  http://www.w3.org/TR/html-markup/input.file.html#input.file-attributes
     *  eg: accept: 'image/*'
     * be selected (Chrome only).
     */
    assignBrowse: function (domNodes, isDirectory, singleFile, attributes) {
      if (typeof domNodes.length === 'undefined') {
        domNodes = [domNodes]
      }
  
      utils.each(domNodes, function (domNode) {
        var input
        if (domNode.tagName === 'INPUT' && domNode.type === 'file') {
          input = domNode
        } else {
          input = document.createElement('input')
          input.setAttribute('type', 'file')
          // display:none - not working in opera 12
          utils.extend(input.style, {
            visibility: 'hidden',
            position: 'absolute',
            width: '1px',
            height: '1px'
          })
          // for opera 12 browser, input must be assigned to a document
          domNode.appendChild(input)
          // https://developer.mozilla.org/en/using_files_from_web_applications)
          // event listener is executed two times
          // first one - original mouse click event
          // second - input.click(), input is inside domNode
          domNode.addEventListener('click', function (e) {
            if (domNode.tagName.toLowerCase() === 'label') {
              return
            }
            input.click()
          }, false)
        }
        if (!this.opts.singleFile && !singleFile) {
          input.setAttribute('multiple', 'multiple')
        }
        if (isDirectory) {
          input.setAttribute('webkitdirectory', 'webkitdirectory')
        }
        attributes && utils.each(attributes, function (value, key) {
          input.setAttribute(key, value)
        })
        // When new files are added, simply append them to the overall list
        var that = this
        input.addEventListener('change', function (e) {
          that._trigger(e.type, e)
          if (e.target.value) {
            that.addFiles(e.target.files, e)
            e.target.value = ''
          }
        }, false)
      }, this)
    },
  
    onDrop: function (evt) {
      this._trigger(evt.type, evt)
      if (this.opts.onDropStopPropagation) {
        evt.stopPropagation()
      }
      evt.preventDefault()
      this._parseDataTransfer(evt.dataTransfer, evt)
    },
  
    _parseDataTransfer: function (dataTransfer, evt) {
      if (dataTransfer.items && dataTransfer.items[0] &&
        dataTransfer.items[0].webkitGetAsEntry) {
        this.webkitReadDataTransfer(dataTransfer, evt)
      } else {
        this.addFiles(dataTransfer.files, evt)
      }
    },
  
    webkitReadDataTransfer: function (dataTransfer, evt) {
      var self = this
      var queue = dataTransfer.items.length
      var files = []
      utils.each(dataTransfer.items, function (item) {
        var entry = item.webkitGetAsEntry()
        if (!entry) {
          decrement()
          return
        }
        if (entry.isFile) {
          // due to a bug in Chrome's File System API impl - #149735
          fileReadSuccess(item.getAsFile(), entry.fullPath)
        } else {
          readDirectory(entry.createReader())
        }
      })
      function readDirectory (reader) {
        reader.readEntries(function (entries) {
          if (entries.length) {
            queue += entries.length
            utils.each(entries, function (entry) {
              if (entry.isFile) {
                var fullPath = entry.fullPath
                entry.file(function (file) {
                  fileReadSuccess(file, fullPath)
                }, readError)
              } else if (entry.isDirectory) {
                readDirectory(entry.createReader())
              }
            })
            readDirectory(reader)
          } else {
            decrement()
          }
        }, readError)
      }
      function fileReadSuccess (file, fullPath) {
        // relative path should not start with "/"
        file.relativePath = fullPath.substring(1)
        files.push(file)
        decrement()
      }
      function readError (fileError) {
        throw fileError
      }
      function decrement () {
        if (--queue === 0) {
          self.addFiles(files, evt)
        }
      }
    },
  
    _assignHelper: function (domNodes, handles, remove) {
      if (typeof domNodes.length === 'undefined') {
        domNodes = [domNodes]
      }
      var evtMethod = remove ? 'removeEventListener' : 'addEventListener'
      utils.each(domNodes, function (domNode) {
        utils.each(handles, function (handler, name) {
          domNode[evtMethod](name, handler, false)
        }, this)
      }, this)
    },
  
    _preventEvent: function (e) {
      utils.preventEvent(e)
      this._trigger(e.type, e)
    },
  
    /**
     * Assign one or more DOM nodes as a drop target.
     * @function
     * @param {Element|Array.<Element>} domNodes
     */
    assignDrop: function (domNodes) {
      this._onDrop = utils.bind(this.onDrop, this)
      this._assignHelper(domNodes, {
        dragover: this.preventEvent,
        dragenter: this.preventEvent,
        dragleave: this.preventEvent,
        drop: this._onDrop
      })
    },
  
    /**
     * Un-assign drop event from DOM nodes
     * @function
     * @param domNodes
     */
    unAssignDrop: function (domNodes) {
      this._assignHelper(domNodes, {
        dragover: this.preventEvent,
        dragenter: this.preventEvent,
        dragleave: this.preventEvent,
        drop: this._onDrop
      }, true)
      this._onDrop = null
    }
  })
  
  module.exports = Uploader
  