text: const Translator = require('../../core/Translator')
  const Plugin = require('../../core/Plugin')
  const Tus = require('../Tus')
  const Client = require('./Client')
  const StatusSocket = require('./Socket')
  
  function defaultGetAssemblyOptions (file, options) {
    return {
      params: options.params,
      signature: options.signature,
      fields: options.fields
    }
  }
  
  const UPPY_SERVER = 'https://api2.transloadit.com/uppy-server'
  // Regex used to check if an uppy-server address is run by Transloadit.
  const TL_UPPY_SERVER = /https?:\/\/api2(?:-\w+)?\.transloadit\.com\/uppy-server/
  
  /**
   * Upload files to Transloadit using Tus.
   */
  module.exports = class Transloadit extends Plugin {
    constructor (uppy, opts) {
      super(uppy, opts)
      this.type = 'uploader'
      this.id = 'Transloadit'
      this.title = 'Transloadit'
  
      const defaultLocale = {
        strings: {
          creatingAssembly: 'Preparing upload...',
          creatingAssemblyFailed: 'Transloadit: Could not create Assembly',
          encoding: 'Encoding...'
        }
      }
  
      const defaultOptions = {
        service: 'https://api2.transloadit.com',
        waitForEncoding: false,
        waitForMetadata: false,
        alwaysRunAssembly: false,
        importFromUploadURLs: false,
        signature: null,
        params: null,
        fields: {},
        getAssemblyOptions: defaultGetAssemblyOptions,
        locale: defaultLocale
      }
  
      this.opts = Object.assign({}, defaultOptions, opts)
  
      this.locale = Object.assign({}, defaultLocale, this.opts.locale)
      this.locale.strings = Object.assign({}, defaultLocale.strings, this.opts.locale.strings)
  
      this.translator = new Translator({ locale: this.locale })
      this.i18n = this.translator.translate.bind(this.translator)
  
      this.prepareUpload = this.prepareUpload.bind(this)
      this.afterUpload = this.afterUpload.bind(this)
      this.handleError = this.handleError.bind(this)
      this.onFileUploadURLAvailable = this.onFileUploadURLAvailable.bind(this)
      this.onRestored = this.onRestored.bind(this)
      this.getPersistentData = this.getPersistentData.bind(this)
  
      if (this.opts.params) {
        this.validateParams(this.opts.params)
      }
  
      this.client = new Client({
        service: this.opts.service
      })
      this.sockets = {}
    }
  
    validateParams (params) {
      if (!params) {
        throw new Error('Transloadit: The `params` option is required.')
      }
  
      if (typeof params === 'string') {
        try {
          params = JSON.parse(params)
        } catch (err) {
          // Tell the user that this is not an Uppy bug!
          err.message = 'Transloadit: The `params` option is a malformed JSON string: ' +
            err.message
          throw err
        }
      }
  
      if (!params.auth || !params.auth.key) {
        throw new Error('Transloadit: The `params.auth.key` option is required. ' +
          'You can find your Transloadit API key at https://transloadit.com/accounts/credentials.')
      }
    }
  
    getAssemblyOptions (fileIDs) {
      const options = this.opts
  
      const normalizeAssemblyOptions = (file, assemblyOptions) => {
        if (Array.isArray(assemblyOptions.fields)) {
          const fieldNames = assemblyOptions.fields
          assemblyOptions.fields = {}
          fieldNames.forEach((fieldName) => {
            assemblyOptions.fields[fieldName] = file.meta[fieldName]
          })
        }
        if (!assemblyOptions.fields) {
          assemblyOptions.fields = {}
        }
        return assemblyOptions
      }
  
      return Promise.all(
        fileIDs.map((fileID) => {
          const file = this.uppy.getFile(fileID)
          const promise = Promise.resolve()
            .then(() => options.getAssemblyOptions(file, options))
            .then((assemblyOptions) => normalizeAssemblyOptions(file, assemblyOptions))
          return promise.then((assemblyOptions) => {
            this.validateParams(assemblyOptions.params)
  
            return {
              fileIDs: [fileID],
              options: assemblyOptions
            }
          })
        })
      )
    }
  
    dedupeAssemblyOptions (list) {
      const dedupeMap = Object.create(null)
      list.forEach(({ fileIDs, options }) => {
        const id = JSON.stringify(options)
        if (dedupeMap[id]) {
          dedupeMap[id].fileIDs.push(...fileIDs)
        } else {
          dedupeMap[id] = {
            options,
            fileIDs: [...fileIDs]
          }
        }
      })
  
      return Object.keys(dedupeMap).map((id) => dedupeMap[id])
    }
  
    createAssembly (fileIDs, uploadID, options) {
      const pluginOptions = this.opts
  
      this.uppy.log('[Transloadit] create Assembly')
  
      return this.client.createAssembly({
        params: options.params,
        fields: options.fields,
        expectedFiles: fileIDs.length,
        signature: options.signature
      }).then((assembly) => {
        // Store the list of assemblies related to this upload.
        const state = this.getPluginState()
        const assemblyList = state.uploadsAssemblies[uploadID]
        const uploadsAssemblies = Object.assign({}, state.uploadsAssemblies, {
          [uploadID]: assemblyList.concat([ assembly.assembly_id ])
        })
  
        this.setPluginState({
          assemblies: Object.assign(state.assemblies, {
            [assembly.assembly_id]: assembly
          }),
          uploadsAssemblies
        })
  
        function attachAssemblyMetadata (file, assembly) {
          // Attach meta parameters for the Tus plugin. See:
          // https://github.com/tus/tusd/wiki/Uploading-to-Transloadit-using-tus#uploading-using-tus
          const tlMeta = {
            assembly_url: assembly.assembly_url,
            filename: file.name,
            fieldname: 'file'
          }
          const meta = Object.assign({}, file.meta, tlMeta)
          // Add assembly-specific Tus endpoint.
          const tus = Object.assign({}, file.tus, {
            endpoint: assembly.tus_url
          })
  
          // Set uppy server location. We only add this, if 'file' has the attribute
          // remote, because this is the criteria to identify remote files.
          // We only replace the hostname for Transloadit's uppy-servers, so that
          // people can self-host them while still using Transloadit for encoding.
          let remote = file.remote
          if (file.remote && TL_UPPY_SERVER.test(file.remote.serverUrl)) {
            let newHost = assembly.uppyserver_url
            let path = file.remote.url.replace(file.remote.serverUrl, '')
            // remove tailing slash
            newHost = newHost.replace(/\/$/, '')
            // remove leading slash
            path = path.replace(/^\//, '')
  
            remote = Object.assign({}, file.remote, {
              serverUrl: newHost,
              url: `${newHost}/${path}`
            })
          }
  
          const transloadit = {
            assembly: assembly.assembly_id
          }
  
          const newFile = Object.assign({}, file, { transloadit })
          // Only configure the Tus plugin if we are uploading straight to Transloadit (the default).
          if (!pluginOptions.importFromUploadURLs) {
            Object.assign(newFile, { meta, tus, remote })
          }
          return newFile
        }
  
        const files = Object.assign({}, this.uppy.getState().files)
        fileIDs.forEach((id) => {
          files[id] = attachAssemblyMetadata(files[id], assembly)
        })
  
        this.uppy.setState({ files })
  
        this.uppy.emit('transloadit:assembly-created', assembly, fileIDs)
  
        return this.connectSocket(assembly)
          .then(() => assembly)
      }).then((assembly) => {
        this.uppy.log(`[Transloadit] Created Assembly ${assembly.assembly_id}`)
        return assembly
      }).catch((err) => {
        // this.uppy.info(this.i18n('creatingAssemblyFailed'), 'error', 0)
        err.message = `${this.i18n('creatingAssemblyFailed')}: ${err.message}`
  
        // Reject the promise.
        throw err
      })
    }
  
    shouldWait () {
      return this.opts.waitForEncoding || this.opts.waitForMetadata
    }
  
    /**
     * Used when `importFromUploadURLs` is enabled: reserves all files in
     * the assembly.
     */
    reserveFiles (assembly, fileIDs) {
      return Promise.all(fileIDs.map((fileID) => {
        const file = this.uppy.getFile(fileID)
        return this.client.reserveFile(assembly, file)
      }))
    }
  
    /**
     * Used when `importFromUploadURLs` is enabled: adds files to the assembly
     * once they have been fully uploaded.
     */
    onFileUploadURLAvailable (file) {
      if (!file || !file.transloadit || !file.transloadit.assembly) {
        return
      }
  
      const state = this.getPluginState()
      const assembly = state.assemblies[file.transloadit.assembly]
  
      this.client.addFile(assembly, file).catch((err) => {
        this.uppy.log(err)
        this.uppy.emit('transloadit:import-error', assembly, file.id, err)
      })
    }
  
    findFile (uploadedFile) {
      const files = this.uppy.getFiles()
      for (let i = 0; i < files.length; i++) {
        const file = files[i]
        // Completed file upload.
        if (file.uploadURL === uploadedFile.tus_upload_url) {
          return file
        }
        // In-progress file upload.
        if (file.tus && file.tus.uploadUrl === uploadedFile.tus_upload_url) {
          return file
        }
        if (!uploadedFile.is_tus_file) {
          // Fingers-crossed check for non-tus uploads, eg imported from S3.
          if (file.name === uploadedFile.name && file.size === uploadedFile.size) {
            return file
          }
        }
      }
    }
  
    onFileUploadComplete (assemblyId, uploadedFile) {
      const state = this.getPluginState()
      const file = this.findFile(uploadedFile)
      if (!file) {
        this.uppy.log('[Transloadit] Couldn’t file the file, it was likely removed in the process')
        return
      }
      this.setPluginState({
        files: Object.assign({}, state.files, {
          [uploadedFile.id]: {
            assembly: assemblyId,
            id: file.id,
            uploadedFile
          }
        })
      })
      this.uppy.emit('transloadit:upload', uploadedFile, this.getAssembly(assemblyId))
    }
  
    onResult (assemblyId, stepName, result) {
      const state = this.getPluginState()
      const file = state.files[result.original_id]
      // The `file` may not exist if an import robot was used instead of a file upload.
      result.localId = file ? file.id : null
  
      const entry = {
        result,
        stepName,
        id: result.id,
        assembly: assemblyId
      }
  
      this.setPluginState({
        results: [...state.results, entry]
      })
      this.uppy.emit('transloadit:result', stepName, result, this.getAssembly(assemblyId))
    }
  
    onAssemblyFinished (url) {
      this.client.getAssemblyStatus(url).then((assembly) => {
        const state = this.getPluginState()
        this.setPluginState({
          assemblies: Object.assign({}, state.assemblies, {
            [assembly.assembly_id]: assembly
          })
        })
        this.uppy.emit('transloadit:complete', assembly)
      })
    }
  
    getPersistentData (setData) {
      const state = this.getPluginState()
      const assemblies = state.assemblies
      const uploadsAssemblies = state.uploadsAssemblies
      const uploads = Object.keys(state.files)
      const results = state.results.map((result) => result.id)
  
      setData({
        [this.id]: {
          assemblies,
          uploadsAssemblies,
          uploads,
          results
        }
      })
    }
  
    /**
     * Emit the necessary events that must have occured to get from the `prevState`,
     * to the current state.
     * For completed uploads, `transloadit:upload` is emitted.
     * For new results, `transloadit:result` is emitted.
     * For completed or errored assemblies, `transloadit:complete` or `transloadit:assembly-error` is emitted.
     */
    emitEventsDiff (prevState) {
      const opts = this.opts
      const state = this.getPluginState()
  
      const emitMissedEvents = () => {
        // Emit events for completed uploads and completed results
        // that we've missed while we were away.
        const newUploads = Object.keys(state.files).filter((fileID) => {
          return !prevState.files.hasOwnProperty(fileID)
        }).map((fileID) => state.files[fileID])
        const newResults = state.results.filter((result) => {
          return !prevState.results.some((prev) => prev.id === result.id)
        })
  
        this.uppy.log('[Transloadit] New fully uploaded files since restore:')
        this.uppy.log(newUploads)
        newUploads.forEach(({ assembly, uploadedFile }) => {
          this.uppy.log(`[Transloadit]  emitting transloadit:upload ${uploadedFile.id}`)
          this.uppy.emit('transloadit:upload', uploadedFile, this.getAssembly(assembly))
        })
        this.uppy.log('[Transloadit] New results since restore:')
        this.uppy.log(newResults)
        newResults.forEach(({ assembly, stepName, result, id }) => {
          this.uppy.log(`[Transloadit]  emitting transloadit:result ${stepName}, ${id}`)
          this.uppy.emit('transloadit:result', stepName, result, this.getAssembly(assembly))
        })
  
        const newAssemblies = state.assemblies
        const previousAssemblies = prevState.assemblies
        this.uppy.log('[Transloadit] Current Assembly status after restore')
        this.uppy.log(newAssemblies)
        this.uppy.log('[Transloadit] Assembly status before restore')
        this.uppy.log(previousAssemblies)
        Object.keys(newAssemblies).forEach((assemblyId) => {
          const oldAssembly = previousAssemblies[assemblyId]
          diffAssemblyStatus(oldAssembly, newAssemblies[assemblyId])
        })
      }
  
      // Emit events for assemblies that have completed or errored while we were away.
      const diffAssemblyStatus = (prev, next) => {
        this.uppy.log('[Transloadit] Diff assemblies')
        this.uppy.log(prev)
        this.uppy.log(next)
  
        if (opts.waitForEncoding && next.ok === 'ASSEMBLY_COMPLETED' && prev.ok !== 'ASSEMBLY_COMPLETED') {
          this.uppy.log(`[Transloadit]  Emitting transloadit:complete for ${next.assembly_id}`)
          this.uppy.log(next)
          this.uppy.emit('transloadit:complete', next)
        } else if (opts.waitForMetadata && next.upload_meta_data_extracted && !prev.upload_meta_data_extracted) {
          this.uppy.log(`[Transloadit]  Emitting transloadit:complete after metadata extraction for ${next.assembly_id}`)
          this.uppy.log(next)
          this.uppy.emit('transloadit:complete', next)
        }
  
        if (next.error && !prev.error) {
          this.uppy.log(`[Transloadit]  !!! Emitting transloadit:assembly-error for ${next.assembly_id}`)
          this.uppy.log(next)
          this.uppy.emit('transloadit:assembly-error', next, new Error(next.message))
        }
      }
  
      emitMissedEvents()
    }
  
    onRestored (pluginData) {
      const savedState = pluginData && pluginData[this.id] ? pluginData[this.id] : {}
      const knownUploads = savedState.files || []
      const knownResults = savedState.results || []
      const previousAssemblies = savedState.assemblies || {}
      const uploadsAssemblies = savedState.uploadsAssemblies || {}
  
      if (Object.keys(uploadsAssemblies).length === 0) {
        // Nothing to restore.
        return
      }
  
      // Fetch up-to-date assembly statuses.
      const loadAssemblies = () => {
        const assemblyIDs = []
        Object.keys(uploadsAssemblies).forEach((uploadID) => {
          assemblyIDs.push(...uploadsAssemblies[uploadID])
        })
  
        return Promise.all(
          assemblyIDs.map((assemblyID) => {
            const url = `https://api2.transloadit.com/assemblies/${assemblyID}`
            return this.client.getAssemblyStatus(url)
          })
        )
      }
  
      const reconnectSockets = (assemblies) => {
        return Promise.all(assemblies.map((assembly) => {
          // No need to connect to the socket if the assembly has completed by now.
          if (assembly.ok === 'ASSEMBLY_COMPLETE') {
            return null
          }
          return this.connectSocket(assembly)
        }))
      }
  
      // Convert loaded assembly statuses to a Transloadit plugin state object.
      const restoreState = (assemblies) => {
        const assembliesById = {}
        const files = {}
        const results = []
        assemblies.forEach((assembly) => {
          assembliesById[assembly.assembly_id] = assembly
  
          assembly.uploads.forEach((uploadedFile) => {
            const file = this.findFile(uploadedFile)
            files[uploadedFile.id] = {
              id: file.id,
              assembly: assembly.assembly_id,
              uploadedFile
            }
          })
  
          const state = this.getPluginState()
          Object.keys(assembly.results).forEach((stepName) => {
            assembly.results[stepName].forEach((result) => {
              const file = state.files[result.original_id]
              result.localId = file ? file.id : null
              results.push({
                id: result.id,
                result,
                stepName,
                assembly: assembly.assembly_id
              })
            })
          })
        })
  
        this.setPluginState({
          assemblies: assembliesById,
          files: files,
          results: results,
          uploadsAssemblies: uploadsAssemblies
        })
      }
  
      // Restore all assembly state.
      this.restored = Promise.resolve()
        .then(loadAssemblies)
        .then((assemblies) => {
          restoreState(assemblies)
          return reconnectSockets(assemblies)
        })
        .then(() => {
          // Return a callback that will be called by `afterUpload`
          // once it has attached event listeners etc.
          const newState = this.getPluginState()
          const previousFiles = {}
          knownUploads.forEach((id) => {
            previousFiles[id] = newState.files[id]
          })
          return () => this.emitEventsDiff({
            assemblies: previousAssemblies,
            files: previousFiles,
            results: newState.results.filter(({ id }) => knownResults.indexOf(id) !== -1),
            uploadsAssemblies
          })
        })
  
      this.restored.then(() => {
        this.restored = null
      })
    }
  
    connectSocket (assembly) {
      const socket = new StatusSocket(
        assembly.websocket_url,
        assembly
      )
      this.sockets[assembly.assembly_id] = socket
  
      socket.on('upload', this.onFileUploadComplete.bind(this, assembly.assembly_id))
      socket.on('error', (error) => {
        this.uppy.emit('transloadit:assembly-error', assembly, error)
      })
  
      socket.on('executing', () => {
        this.uppy.emit('transloadit:assembly-executing', assembly)
      })
  
      if (this.opts.waitForEncoding) {
        socket.on('result', this.onResult.bind(this, assembly.assembly_id))
      }
  
      if (this.opts.waitForEncoding) {
        socket.on('finished', () => {
          this.onAssemblyFinished(assembly.assembly_ssl_url)
        })
      } else if (this.opts.waitForMetadata) {
        socket.on('metadata', () => {
          this.onAssemblyFinished(assembly.assembly_ssl_url)
        })
      }
  
      return new Promise((resolve, reject) => {
        socket.on('connect', resolve)
        socket.on('error', reject)
      }).then(() => {
        this.uppy.log('[Transloadit] Socket is ready')
      })
    }
  
    prepareUpload (fileIDs, uploadID) {
      // Only use files without errors
      fileIDs = fileIDs.filter((file) => !file.error)
  
      fileIDs.forEach((fileID) => {
        const file = this.uppy.getFile(fileID)
        this.uppy.emit('preprocess-progress', file, {
          mode: 'indeterminate',
          message: this.i18n('creatingAssembly')
        })
      })
  
      const createAssembly = ({ fileIDs, options }) => {
        return this.createAssembly(fileIDs, uploadID, options).then((assembly) => {
          if (this.opts.importFromUploadURLs) {
            return this.reserveFiles(assembly, fileIDs)
          }
        }).then(() => {
          fileIDs.forEach((fileID) => {
            const file = this.uppy.getFile(fileID)
            this.uppy.emit('preprocess-complete', file)
          })
        }).catch((err) => {
          // Clear preprocessing state when the assembly could not be created,
          // otherwise the UI gets confused about the lingering progress keys
          fileIDs.forEach((fileID) => {
            const file = this.uppy.getFile(fileID)
            this.uppy.emit('preprocess-complete', file)
            this.uppy.emit('upload-error', file, err)
          })
          throw err
        })
      }
  
      const state = this.getPluginState()
      const uploadsAssemblies = Object.assign({},
        state.uploadsAssemblies,
        { [uploadID]: [] })
      this.setPluginState({ uploadsAssemblies })
  
      let optionsPromise
      if (fileIDs.length > 0) {
        optionsPromise = Promise.resolve(this.getAssemblyOptions(fileIDs))
          .then((allOptions) => this.dedupeAssemblyOptions(allOptions))
      } else if (this.opts.alwaysRunAssembly) {
        optionsPromise = Promise.resolve(
          this.opts.getAssemblyOptions(null, this.opts)
        ).then((options) => {
          this.validateParams(options.params)
          return [
            { fileIDs, options }
          ]
        })
      } else {
        // If there are no files and we do not `alwaysRunAssembly`,
        // don't do anything.
        return Promise.resolve()
      }
  
      return optionsPromise.then(
        (assemblies) => Promise.all(
          assemblies.map(createAssembly)
        ),
        // If something went wrong before any assemblies could be created,
        // clear all processing state.
        (err) => {
          fileIDs.forEach((fileID) => {
            const file = this.uppy.getFile(fileID)
            this.uppy.emit('preprocess-complete', file)
            this.uppy.emit('upload-error', file, err)
          })
          throw err
        }
      )
    }
  
    afterUpload (fileIDs, uploadID) {
      // Only use files without errors
      fileIDs = fileIDs.filter((file) => !file.error)
  
      const state = this.getPluginState()
  
      // If we're still restoring state, wait for that to be done.
      if (this.restored) {
        return this.restored.then((emitMissedEvents) => {
          const promise = this.afterUpload(fileIDs, uploadID)
          emitMissedEvents()
          return promise
        })
      }
  
      const assemblyIDs = state.uploadsAssemblies[uploadID]
  
      // If we don't have to wait for encoding metadata or results, we can close
      // the socket immediately and finish the upload.
      if (!this.shouldWait()) {
        assemblyIDs.forEach((assemblyID) => {
          const socket = this.sockets[assemblyID]
          socket.close()
        })
        const assemblies = assemblyIDs.map((id) => this.getAssembly(id))
        this.uppy.addResultData(uploadID, { transloadit: assemblies })
        return Promise.resolve()
      }
  
      // If no assemblies were created for this upload, we also do not have to wait.
      // There's also no sockets or anything to close, so just return immediately.
      if (assemblyIDs.length === 0) {
        this.uppy.addResultData(uploadID, { transloadit: [] })
        return Promise.resolve()
      }
  
      let finishedAssemblies = 0
  
      return new Promise((resolve, reject) => {
        fileIDs.forEach((fileID) => {
          const file = this.uppy.getFile(fileID)
          this.uppy.emit('postprocess-progress', file, {
            mode: 'indeterminate',
            message: this.i18n('encoding')
          })
        })
  
        const onAssemblyFinished = (assembly) => {
          // An assembly for a different upload just finished. We can ignore it.
          if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
            this.uppy.log(`[Transloadit] afterUpload(): Ignoring finished Assembly ${assembly.assembly_id}`)
            return
          }
          this.uppy.log(`[Transloadit] afterUpload(): Got Assembly finish ${assembly.assembly_id}`)
  
          // TODO set the `file.uploadURL` to a result?
          // We will probably need an option here so the plugin user can tell us
          // which result to pick…?
  
          const files = this.getAssemblyFiles(assembly.assembly_id)
          files.forEach((file) => {
            this.uppy.emit('postprocess-complete', file)
          })
  
          checkAllComplete()
        }
  
        const onAssemblyError = (assembly, error) => {
          // An assembly for a different upload just errored. We can ignore it.
          if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
            this.uppy.log(`[Transloadit] afterUpload(): Ignoring errored Assembly ${assembly.assembly_id}`)
            return
          }
          this.uppy.log(`[Transloadit] afterUpload(): Got Assembly error ${assembly.assembly_id}`)
          this.uppy.log(error)
          // this.uppy.info({
          //   message: error.code,
          //   details: error.status.reason
          // }, 'error', 5000)
  
          // Clear postprocessing state for all our files.
          const files = this.getAssemblyFiles(assembly.assembly_id)
          files.forEach((file) => {
            // TODO Maybe make a postprocess-error event here?
            this.uppy.emit('upload-error', file, error)
  
            this.uppy.emit('postprocess-complete', file)
          })
  
          checkAllComplete()
        }
  
        const onImportError = (assembly, fileID, error) => {
          if (assemblyIDs.indexOf(assembly.assembly_id) === -1) {
            return
          }
  
          // Not sure if we should be doing something when it's just one file failing.
          // ATM, the only options are 1) ignoring or 2) failing the entire upload.
          // I think failing the upload is better than silently ignoring.
          // In the future we should maybe have a way to resolve uploads with some failures,
          // like returning an object with `{ successful, failed }` uploads.
          onAssemblyError(assembly, error)
        }
  
        const checkAllComplete = () => {
          finishedAssemblies += 1
          if (finishedAssemblies === assemblyIDs.length) {
            // We're done, these listeners can be removed
            removeListeners()
            const assemblies = assemblyIDs.map((id) => this.getAssembly(id))
            this.uppy.addResultData(uploadID, { transloadit: assemblies })
            resolve()
          }
        }
  
        const removeListeners = () => {
          this.uppy.off('transloadit:complete', onAssemblyFinished)
          this.uppy.off('transloadit:assembly-error', onAssemblyError)
          this.uppy.off('transloadit:import-error', onImportError)
        }
  
        this.uppy.on('transloadit:complete', onAssemblyFinished)
        this.uppy.on('transloadit:assembly-error', onAssemblyError)
        this.uppy.on('transloadit:import-error', onImportError)
      }).then((result) => {
        // Clean up uploadID → assemblyIDs, they're no longer going to be used anywhere.
        const state = this.getPluginState()
        const uploadsAssemblies = Object.assign({}, state.uploadsAssemblies)
        delete uploadsAssemblies[uploadID]
        this.setPluginState({ uploadsAssemblies })
  
        return result
      })
    }
  
    handleError (err, uploadID) {
      this.uppy.log('[Transloadit] handleError')
      this.uppy.log(err)
      this.uppy.log(uploadID)
      const state = this.getPluginState()
      const assemblyIDs = state.uploadsAssemblies[uploadID]
  
      assemblyIDs.forEach((assemblyID) => {
        if (this.sockets[assemblyID]) {
          this.sockets[assemblyID].close()
        }
      })
    }
  
    install () {
      this.uppy.addPreProcessor(this.prepareUpload)
      this.uppy.addPostProcessor(this.afterUpload)
  
      // We may need to close socket.io connections on error.
      this.uppy.on('error', this.handleError)
  
      if (this.opts.importFromUploadURLs) {
        // No uploader needed when importing; instead we take the upload URL from an existing uploader.
        this.uppy.on('upload-success', this.onFileUploadURLAvailable)
      } else {
        this.uppy.use(Tus, {
          // Disable tus-js-client fingerprinting, otherwise uploading the same file at different times
          // will upload to the same assembly.
          resume: false,
          // Disable Uppy Server's retry optimisation; we need to change the endpoint on retry
          // so it can't just reuse the same tus.Upload instance server-side.
          useFastRemoteRetry: false,
          // Only send assembly metadata to the tus endpoint.
          metaFields: ['assembly_url', 'filename', 'fieldname']
        })
      }
  
      this.uppy.on('restore:get-data', this.getPersistentData)
      this.uppy.on('restored', this.onRestored)
  
      this.setPluginState({
        // Contains assembly status objects, indexed by their ID.
        assemblies: {},
        // Contains arrays of assembly IDs, indexed by the upload ID that they belong to.
        uploadsAssemblies: {},
        // Contains file data from Transloadit, indexed by their Transloadit-assigned ID.
        files: {},
        // Contains result data from Transloadit.
        results: []
      })
    }
  
    uninstall () {
      this.uppy.removePreProcessor(this.prepareUpload)
      this.uppy.removePostProcessor(this.afterUpload)
      this.uppy.off('error', this.handleError)
  
      if (this.opts.importFromUploadURLs) {
        this.uppy.off('upload-success', this.onFileUploadURLAvailable)
      }
    }
  
    getAssembly (id) {
      const state = this.getPluginState()
      return state.assemblies[id]
    }
  
    getAssemblyFiles (assemblyID) {
      return this.uppy.getFiles().filter((file) => {
        return file && file.transloadit && file.transloadit.assembly === assemblyID
      })
    }
  }
  
  module.exports.UPPY_SERVER = UPPY_SERVER
  