const Plugin = require('./Plugin')
const tus = require('tus-js-client')
const UppySocket = require('../core/UppySocket')
const {
  emitSocketProgress,
  getSocketHost,
  settle
} = require('../core/Utils')
require('whatwg-fetch')

// Extracted from https://github.com/tus/tus-js-client/blob/master/lib/upload.js#L13
// excepted we removed 'fingerprint' key to avoid adding more dependencies
const tusDefaultOptions = {
  endpoint: '',
  resume: true,
  onProgress: null,
  onChunkComplete: null,
  onSuccess: null,
  onError: null,
  headers: {},
  chunkSize: Infinity,
  withCredentials: false,
  uploadUrl: null,
  uploadSize: null,
  overridePatchMethod: false,
  retryDelays: null
}

/**
 * Create a wrapper around an event emitter with a `remove` method to remove
 * all events that were added using the wrapped emitter.
 */
function createEventTracker (emitter) {
  const events = []
  return {
    on (event, fn) {
      events.push([ event, fn ])
      return emitter.on(event, fn)
    },
    remove () {
      events.forEach(([ event, fn ]) => {
        emitter.off(event, fn)
      })
    }
  }
}

/**
 * Tus resumable file uploader
 *
 */
module.exports = class Tus10 extends Plugin {
  constructor (core, opts) {
    super(core, opts)
    this.type = 'uploader'
    this.id = 'Tus'
    this.title = 'Tus'

    // set default options
    const defaultOptions = {
      resume: true,
      autoRetry: true,
      retryDelays: [0, 1000, 3000, 5000]
    }

    // merge default options with the ones set by user
    this.opts = Object.assign({}, defaultOptions, opts)

    this.uploaders = Object.create(null)
    this.uploaderEvents = Object.create(null)
    this.uploaderSockets = Object.create(null)

    this.handleResetProgress = this.handleResetProgress.bind(this)
    this.handleUpload = this.handleUpload.bind(this)
  }

  handleResetProgress () {
    const files = Object.assign({}, this.core.state.files)
    Object.keys(files).forEach((fileID) => {
      // Only clone the file object if it has a Tus `uploadUrl` attached.
      if (files[fileID].tus && files[fileID].tus.uploadUrl) {
        const tusState = Object.assign({}, files[fileID].tus)
        delete tusState.uploadUrl
        files[fileID] = Object.assign({}, files[fileID], { tus: tusState })
      }
    })

    this.core.setState({ files })
  }

  /**
   * Clean up all references for a file's upload: the tus.Upload instance,
   * any events related to the file, and the uppy-server WebSocket connection.
   */
  resetUploaderReferences (fileID) {
    if (this.uploaders[fileID]) {
      this.uploaders[fileID].abort()
      this.uploaders[fileID] = null
    }
    if (this.uploaderEvents[fileID]) {
      this.uploaderEvents[fileID].remove()
      this.uploaderEvents[fileID] = null
    }
    if (this.uploaderSockets[fileID]) {
      this.uploaderSockets[fileID].close()
      this.uploaderSockets[fileID] = null
    }
  }

  /**
   * Create a new Tus upload
   *
   * @param {object} file for use with upload
   * @param {integer} current file in a queue
   * @param {integer} total number of files in a queue
   * @returns {Promise}
   */
  upload (file, current, total) {
    this.core.log(`uploading ${current} of ${total}`)

    this.resetUploaderReferences(file.id)

    // Create a new tus upload
    return new Promise((resolve, reject) => {
      const optsTus = Object.assign(
        {},
        tusDefaultOptions,
        this.opts,
        // Install file-specific upload overrides.
        file.tus || {}
      )

      optsTus.onError = (err) => {
        this.core.log(err)
        this.core.emit('core:upload-error', file.id, err)
        err.message = `Failed because: ${err.message}`

        this.resetUploaderReferences(file.id)
        reject(err)
      }

      optsTus.onProgress = (bytesUploaded, bytesTotal) => {
        this.onReceiveUploadUrl(file, upload.url)
        this.core.emit('core:upload-progress', {
          uploader: this,
          id: file.id,
          bytesUploaded: bytesUploaded,
          bytesTotal: bytesTotal
        })
      }

      optsTus.onSuccess = () => {
        this.core.emit('core:upload-success', file.id, upload, upload.url)

        if (upload.url) {
          this.core.log('Download ' + upload.file.name + ' from ' + upload.url)
        }

        this.resetUploaderReferences(file.id)
        resolve(upload)
      }
      optsTus.metadata = file.meta

      const upload = new tus.Upload(file.data, optsTus)
      this.uploaders[file.id] = upload
      this.uploaderEvents[file.id] = createEventTracker(this.core)

      this.onFileRemove(file.id, (targetFileID) => {
        this.resetUploaderReferences(file.id)
        resolve(`upload ${targetFileID} was removed`)
      })

      this.onPause(file.id, (isPaused) => {
        isPaused ? upload.abort() : upload.start()
      })

      this.onPauseAll(file.id, () => {
        upload.abort()
      })

      this.onCancelAll(file.id, () => {
        this.resetUploaderReferences(file.id)
      })

      this.onResumeAll(file.id, () => {
        if (file.error) {
          upload.abort()
        }
        upload.start()
      })

      upload.start()
      this.core.emit('core:upload-started', file.id, upload)
    })
  }

  uploadRemote (file, current, total) {
    this.resetUploaderReferences(file.id)

    return new Promise((resolve, reject) => {
      this.core.log(file.remote.url)
      if (file.serverToken) {
        this.connectToServerSocket(file)
      } else {
        let endpoint = this.opts.endpoint
        if (file.tus && file.tus.endpoint) {
          endpoint = file.tus.endpoint
        }

        this.core.emitter.emit('core:upload-started', file.id)

        fetch(file.remote.url, {
          method: 'post',
          credentials: 'include',
          headers: {
            'Accept': 'application/json',
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(Object.assign({}, file.remote.body, {
            endpoint,
            protocol: 'tus',
            size: file.data.size,
            metadata: file.meta
          }))
        })
        .then((res) => {
          if (res.status < 200 && res.status > 300) {
            return reject(res.statusText)
          }

          res.json().then((data) => {
            const token = data.token
            file = this.getFile(file.id)
            file.serverToken = token
            this.updateFile(file)
            this.connectToServerSocket(file)
            resolve()
          })
        })
      }
    })
  }

  connectToServerSocket (file) {
    const token = file.serverToken
    const host = getSocketHost(file.remote.host)
    const socket = new UppySocket({ target: `${host}/api/${token}` })
    this.uploaderSockets[file.id] = socket
    this.uploaderEvents[file.id] = createEventTracker(this.core)

    this.onFileRemove(file.id, () => socket.send('pause', {}))

    this.onPause(file.id, (isPaused) => {
      isPaused ? socket.send('pause', {}) : socket.send('resume', {})
    })

    this.onPauseAll(file.id, () => socket.send('pause', {}))

    this.onCancelAll(file.id, () => socket.send('pause', {}))

    this.onResumeAll(file.id, () => {
      if (file.error) {
        socket.send('pause', {})
      }
      socket.send('resume', {})
    })

    this.onRetry(file.id, () => {
      socket.send('pause', {})
      socket.send('resume', {})
    })

    this.onRetryAll(file.id, () => {
      socket.send('pause', {})
      socket.send('resume', {})
    })

    socket.on('progress', (progressData) => emitSocketProgress(this, progressData, file))

    socket.on('success', (data) => {
      this.core.emitter.emit('core:upload-success', file.id, data, data.url)
      this.resetUploaderReferences(file.id)
    })
  }

  getFile (fileID) {
    return this.core.state.files[fileID]
  }

  updateFile (file) {
    const files = Object.assign({}, this.core.state.files, {
      [file.id]: file
    })
    this.core.setState({ files })
  }

  onReceiveUploadUrl (file, uploadURL) {
    const currentFile = this.getFile(file.id)
    if (!currentFile) return
    // Only do the update if we didn't have an upload URL yet.
    if (!currentFile.tus || currentFile.tus.uploadUrl !== uploadURL) {
      const newFile = Object.assign({}, currentFile, {
        tus: Object.assign({}, currentFile.tus, {
          uploadUrl: uploadURL
        })
      })
      this.updateFile(newFile)
    }
  }

  onFileRemove (fileID, cb) {
    this.uploaderEvents[fileID].on('core:file-removed', (targetFileID) => {
      if (fileID === targetFileID) cb(targetFileID)
    })
  }

  onPause (fileID, cb) {
    this.uploaderEvents[fileID].on('core:upload-pause', (targetFileID, isPaused) => {
      if (fileID === targetFileID) {
        // const isPaused = this.core.pauseResume(fileID)
        cb(isPaused)
      }
    })
  }

  onRetry (fileID, cb) {
    this.uploaderEvents[fileID].on('core:upload-retry', (targetFileID) => {
      if (fileID === targetFileID) {
        cb()
      }
    })
  }

  onRetryAll (fileID, cb) {
    this.uploaderEvents[fileID].on('core:retry-all', (filesToRetry) => {
      if (!this.core.getFile(fileID)) return
      cb()
    })
  }

  onPauseAll (fileID, cb) {
    this.uploaderEvents[fileID].on('core:pause-all', () => {
      if (!this.core.getFile(fileID)) return
      cb()
    })
  }

  onCancelAll (fileID, cb) {
    this.uploaderEvents[fileID].on('core:cancel-all', () => {
      if (!this.core.getFile(fileID)) return
      cb()
    })
  }

  onResumeAll (fileID, cb) {
    this.uploaderEvents[fileID].on('core:resume-all', () => {
      if (!this.core.getFile(fileID)) return
      cb()
    })
  }

  uploadFiles (files) {
    const promises = files.map((file, index) => {
      const current = parseInt(index, 10) + 1
      const total = files.length

      if (!file.isRemote) {
        return this.upload(file, current, total)
      } else {
        return this.uploadRemote(file, current, total)
      }
    })

    return settle(promises)
  }

  handleUpload (fileIDs) {
    if (fileIDs.length === 0) {
      this.core.log('Tus: no files to upload!')
      return Promise.resolve()
    }

    this.core.log('Tus is uploading...')
    const filesToUpload = fileIDs.map((fileID) => this.core.getFile(fileID))

    return this.uploadFiles(filesToUpload)
  }

  addResumableUploadsCapabilityFlag () {
    const newCapabilities = Object.assign({}, this.core.getState().capabilities)
    newCapabilities.resumableUploads = true
    this.core.setState({
      capabilities: newCapabilities
    })
  }

  install () {
    this.addResumableUploadsCapabilityFlag()
    this.core.addUploader(this.handleUpload)

    this.core.on('core:reset-progress', this.handleResetProgress)

    if (this.opts.autoRetry) {
      this.core.on('back-online', this.core.retryAll)
    }
  }

  uninstall () {
    this.core.removeUploader(this.handleUpload)

    if (this.opts.autoRetry) {
      this.core.off('back-online', this.core.retryAll)
    }
  }
}