import { ref, computed } from 'vue'

function getFn(prop) {
  return typeof prop === 'function' ? prop : () => prop
}

const name = 'QUploader'

const componentProps = {
  url: [Function, String],
  method: {
    type: [Function, String],
    default: 'POST'
  },
  fieldName: {
    type: [Function, String],
    default: () => file => file.name
  },
  headers: [Function, Array],
  formFields: [Function, Array],
  withCredentials: [Function, Boolean],
  sendRaw: [Function, Boolean],

  batch: [Function, Boolean],
  factory: Function
}

const emits = ['factoryFailed', 'uploaded', 'failed', 'uploading']

function injectPlugin({ props, emit, helpers }) {
  const xhrs = ref([])
  const promises = ref([])
  const workingThreads = ref(0)

  const xhrProps = computed(() => ({
    url: getFn(props.url),
    method: getFn(props.method),
    headers: getFn(props.headers),
    formFields: getFn(props.formFields),
    fieldName: getFn(props.fieldName),
    withCredentials: getFn(props.withCredentials),
    sendRaw: getFn(props.sendRaw),
    batch: getFn(props.batch)
  }))

  const isUploading = computed(() => workingThreads.value > 0)
  const isBusy = computed(() => promises.value.length !== 0)

  let abortPromises

  function abort() {
    xhrs.value.forEach(x => {
      x.abort()
    })

    if (promises.value.length !== 0) {
      abortPromises = true
    }
  }

  function upload() {
    const queue = helpers.queuedFiles.value.slice(0)
    helpers.queuedFiles.value = []

    if (xhrProps.value.batch(queue)) {
      runFactory(queue)
    } else {
      queue.forEach(file => {
        runFactory([file])
      })
    }
  }

  function runFactory(files) {
    workingThreads.value++

    if (typeof props.factory !== 'function') {
      performUpload(files, {})
      return
    }

    const res = props.factory(files)

    if (!res) {
      emit(
        'factoryFailed',
        new Error('QUploader: factory() does not return properly'),
        files
      )
      workingThreads.value--
    } else if (
      typeof res.catch === 'function' &&
      typeof res.then === 'function'
    ) {
      promises.value.push(res)

      const failed = err => {
        if (helpers.isAlive() === true) {
          promises.value = promises.value.filter(p => p !== res)

          if (promises.value.length === 0) {
            abortPromises = false
          }

          helpers.queuedFiles.value = helpers.queuedFiles.value.concat(files)
          files.forEach(f => {
            helpers.updateFileStatus(f, 'failed')
          })

          emit('factoryFailed', err, files)
          workingThreads.value--
        }
      }

      res
        .then(factory => {
          if (abortPromises === true) {
            failed(new Error('Aborted'))
          } else if (helpers.isAlive() === true) {
            promises.value = promises.value.filter(p => p !== res)
            performUpload(files, factory)
          }
        })
        .catch(failed)
    } else {
      performUpload(files, res || {})
    }
  }

  function performUpload(files, factory) {
    const form = new FormData(),
      xhr = new XMLHttpRequest()

    const getProp = (propName, arg) =>
      factory[propName] !== void 0
        ? getFn(factory[propName])(arg)
        : xhrProps.value[propName](arg)

    const url = getProp('url', files)

    if (!url) {
      console.error('q-uploader: invalid or no URL specified')
      workingThreads.value--
      return
    }

    const fields = getProp('formFields', files)
    if (fields !== void 0) {
      fields.forEach(field => {
        form.append(field.name, field.value)
      })
    }

    let uploadIndex = 0,
      uploadIndexSize = 0,
      localUploadedSize = 0,
      maxUploadSize = 0,
      aborted

    xhr.upload.addEventListener(
      'progress',
      e => {
        if (aborted === true) return

        const loaded = Math.min(maxUploadSize, e.loaded)

        helpers.uploadedSize.value += loaded - localUploadedSize
        localUploadedSize = loaded

        let size = localUploadedSize - uploadIndexSize
        for (let i = uploadIndex; size > 0 && i < files.length; i++) {
          const file = files[i],
            uploaded = size > file.size

          if (uploaded) {
            size -= file.size
            uploadIndex++
            uploadIndexSize += file.size
            helpers.updateFileStatus(file, 'uploading', file.size)
          } else {
            helpers.updateFileStatus(file, 'uploading', size)
            return
          }
        }
      },
      false
    )

    xhr.onreadystatechange = () => {
      if (xhr.readyState < 4) return

      if (xhr.status && xhr.status < 400) {
        helpers.uploadedFiles.value = helpers.uploadedFiles.value.concat(files)
        files.forEach(f => {
          helpers.updateFileStatus(f, 'uploaded')
        })
        emit('uploaded', { files, xhr })
      } else {
        aborted = true
        helpers.uploadedSize.value -= localUploadedSize
        helpers.queuedFiles.value = helpers.queuedFiles.value.concat(files)
        files.forEach(f => {
          helpers.updateFileStatus(f, 'failed')
        })
        emit('failed', { files, xhr })
      }

      workingThreads.value--
      xhrs.value = xhrs.value.filter(x => x !== xhr)
    }

    xhr.open(getProp('method', files), url)

    if (getProp('withCredentials', files) === true) {
      xhr.withCredentials = true
    }

    const headers = getProp('headers', files)
    if (headers !== void 0) {
      headers.forEach(head => {
        xhr.setRequestHeader(head.name, head.value)
      })
    }

    const sendRaw = getProp('sendRaw', files)

    files.forEach(file => {
      helpers.updateFileStatus(file, 'uploading', 0)
      if (sendRaw !== true) {
        form.append(getProp('fieldName', file), file, file.name)
      }
      file.xhr = xhr
      file.__abort = () => {
        xhr.abort()
      }
      maxUploadSize += file.size
    })

    emit('uploading', { files, xhr })
    xhrs.value.push(xhr)

    if (sendRaw === true) {
      xhr.send(new Blob(files))
    } else {
      xhr.send(form)
    }
  }

  return {
    isUploading,
    isBusy,

    abort,
    upload
  }
}

export default {
  name,
  props: componentProps,
  emits,
  injectPlugin
}
