<template>
  <div>
    <div v-if="!props.disabled && props.text" style="display: inline-flex;" :class="{ doing: doing || !uploadKey }"
      @click="open()">
      <slot name="button"><el-button>{{ props.text }}</el-button></slot>
    </div>
    <input ref="fileInputRef" v-if="!props.disabled" :accept="props.accept" @change="fileInputChange" type="file"
      style="position: absolute; top:-100vh;" :multiple="props.max != 1" />
    <div class="list">
      <slot :list="list">
        <div v-for="(item, index) in data" :key="index" class="item"
          :class="{ error: item.error, abort: item.abort, border: props.border }"
          :style="{ '--ps': (item.uploading?.loaded || 0) * 100 / (item.uploading?.total || 1) + '%' }">
          <div class="icon" :class="['icon-' + item.ext]"></div>
          <a v-if="item.raw" href="javascript:void(0)">{{ item.name }}</a>
          <a v-else :href="baseUrl + base + 'download?' + request.params({ url: item.url, key: uploadKey }).toString()"
            target="_blank">{{ item.name }}</a>
          <div v-if="item.uploading && !item.abort" class="btn" @click="item.uploading.abort()">取消</div>
          <div v-if="item.url && !props.disabled" @click="item.remove()" class="btn">删除</div>
        </div>
      </slot>
    </div>

  </div>
</template>
<script setup>
import { ref, onMounted, onUnmounted, computed, getCurrentInstance } from 'vue'
import { request } from '@/utils'
import { ElMessage } from 'element-plus'

const base = "/file/upload/"

const baseUrl = import.meta.env.VITE_APP_BASE_API

const { proxy } = getCurrentInstance()

const props = defineProps({
  /**
   * 上传的文件
   */
  modelValue: {
    type: [Array, String],
    required: true
  },
  /**
   * 上传的数量,0：表示无限制,1：表示单文件上传
   */
  max: {
    type: Number,
    default: 0
  },
  /**
   * 获取上传凭证的方法
   */
  getUploadKey: {
    type: Function,
    default: async () => {
      let r = await request.get("/uploadKey")
      return r.data
    }
  },
  /**
   * 上传路径前缀
   */
  prefix: {
    type: String,
    default: "default"
  },
  /**
   * 是否保存上传文件名
   * keepFilename
   */
  keepFilename: {
    type: Boolean,
    default: true
  },
  /**
   * 禁止上传（文件预览模式）
   */
  disabled: {
    type: Boolean,
    default: false
  },
  /**
   * 按钮文字
   */
  text: {
    type: String,
    default: '上传文件'
  },
  /**
   * 上传中?
   */
  uploading: {
    type: Boolean,
    default: false
  },
  /**
   * 允许上传的文件扩展名列表，默认：'' ,office:'.doc,.docx,.ppt,.pptx,.xls,.xlsx'
   */
  accept: {
    type: String,
    default: ''
  },
  /**
   * 允许上传的文件最大大小,0表示不限制，默认：0
   */
  maxSize: {
    type: Number,
    default: 0
  },
  /**
   * 并行上传数量：默认：2
   */
  threadNum: {
    type: Number,
    default: 2
  },
  /** 重试次数，默认：3 */
  retry: {
    type: Number,
    default: 3
  },
  /**
   * 分片大小，默认：5Mb
   */
  chunksize: {
    type: Number,
    default: 5 * 1024 * 1024
  },
  border: {
    type: Boolean,
    default: true
  }

})


const emit = defineEmits(["update:modelValue", "change", "update:uploading"]);

const mv = computed({
  get() {
    return props.modelValue
  },
  set(value) {
    emit("update:modelValue", value)
    emit("change", value)
  }
})
const uploading = computed({
  get() {
    return props.uploading
  },
  set(value) {
    emit("update:uploading", value)
  }
})

const data = ref([])

const uploadKey = ref("");

const getExt = (url) => {
  return url.substring(url.lastIndexOf(".") + 1)
}

const getName = (url) => {
  return url.substring(url.lastIndexOf("/") + 1)
}

onMounted(async () => {
  uploadKey.value = await props.getUploadKey()
  if (props.max == 1) {
    data.value = []
    if (mv.value) {
      data.value = [{ url: mv.value, ext: getExt(mv.value), name: getName(mv.value) }]
    }
  } else {
    data.value = []
    if (mv.value) {
      data.value = mv.value.map(a => ({ url: a, ext: getExt(a), name: getName(a) }))
    }
  }
  for (let one of data.value) {
    one.remove = () => {
      remove(one)
    }
  }
})

const remove = (one) => {
  data.value.splice(data.value.indexOf(one), 1)
  if (props.max == 1) {
    mv.value = data.value[0]?.url
  } else {
    mv.value = data.value.filter(a => 'url' in a).map(a => a.url)
  }
  request.post(base + "remove?r=" + Math.random(), request.params({ key: uploadKey.value, url: one.url }))
}

onUnmounted(async () => {
  await request.get(base + "removeUploadKey-" + uploadKey.value)
})
window.addEventListener('beforeunload', async () => {
  await request.get(base + "removeUploadKey-" + uploadKey.value)
})


/**
 * 刷新上传凭证
 */
const reloadUploadKey = async () => {
  request.get(base + "removeUploadKey-" + uploadKey.value)
  uploadKey.value = await props.getUploadKey()
}

const fileInputRef = ref(null);

/**
 * 打开文件选择对话框
 */
const open = () => {
  if (doing.value) {
    return
  }
  try {
    fileInputRef.value.click();
  } catch (e) { }
}

const fileInputChange = () => {

  let files = fileInputRef.value.files
  if (files.length > 0) {
    // fileInputRef.value.value = ''
    for (let file of files) {
      addUploadFile(file)
    }
    uploadFiles()
  }
}

const addUploadFile = async (file) => {
  if (props.max > 1 && props.max == data.value.length) {
    ElMessage.error('最多只允许上传' + props.max + '个文件')
    return
  }
  // 判断类型
  if (props.accept) {
    let ext = file.name.toLowerCase().substring(file.name.lastIndexOf("."))
    if (props.accept.indexOf(ext) == -1) {
      ElMessage.error(file.name + '类型错误')
      return
    }
  }
  if (props.maxSize > 0) {
    if (file.size > props.maxSize) {
      ElMessage.error(file.name + '大小超过了' + props.maxSize.toFileSize())
      return
    }
  }
  if (props.max == 1) {
    data.value.length = 0
  }


  data.value.push({ raw: file, ext: getExt(file.name), name: file.name })

}

let doing = ref(false);
const uploadFiles = async () => {
  if (doing.value) {
    return
  }
  doing.value = true
  uploading.value = true
  await reloadUploadKey()
  let index = 0
  let doOne = async () => {
    while (index < data.value.length) {
      await uploadFile(index++)
    }
  }

  let a = [];
  for (let i = 0; i < props.threadNum; i++) {
    a.push(doOne())
  }
  Promise.all(a).then(() => {
  }).finally(() => {
    data.value = data.value.filter(a => !a.error && !a.abort)
    doing.value = false
    uploading.value = false
  })
}



const uploadFile = async (index, retry = 0) => {
  let one = data.value[index];
  if (!one) {
    return
  }
  if (one.url) {
    return
  }
  if (retry >= props.retry) {
    one.error = true
    return
  }
  if (one.uploading || one.error || one.abort) {
    return
  }
  retry++

  one.uploading = { loaded: 0, total: one.raw.size }
  one.uploading.controller = new AbortController();
  one.uploading.abort = () => {
    one.uploading.controller.abort()
    if (one.raw.size > props.chunksize) {
      request.post(base + 'multipartUploadAbort?uploadId=' + one.uploading.uploadId)
    }
  }

  one.remove = () => {
    remove(one)
  }

  if (one.raw.size <= props.chunksize) {

    let formData = new FormData()
    formData.append("prefix", props.prefix)
    formData.append("key", uploadKey.value)
    formData.append("keepFilename", props.keepFilename)
    formData.append("file", one.raw)

    let onUploadProgress = (event) => {
      one.uploading.loaded = event.loaded

    }

    try {
      let r = await request.post(base + "?r=" + Math.random(), formData, {
        onUploadProgress,
        timeout: 600000,
        showLoading: false,
        signal: one.uploading.controller.signal
      })
      if (r.code == 200) {
        delete one.raw
        delete one.uploading
        one.url = r.data
        if (props.max == 1) {
          mv.value = r.data
        } else {
          mv.value = data.value.filter(a => 'url' in a).map(a => a.url)
        }
      } else {
        delete one.uploading
        await uploadFile(index, retry)
      }
    } catch (e) {
      delete one.uploading
      await uploadFile(index, retry)
    }
  } else {
    try {
      await uploadFileBig(one)
    } catch (e) {
      delete one.uploading
      await uploadFile(index, retry)
    }
  }
}

/**
 * 大文件上传
 * @param one 
 */
const uploadFileBig = async (one) => {
  let chunksList = chunkFile(one.raw, props.chunksize)
  let r = await request.post(base + "multipartUploadInit?r=" + Math.random(), request.params({ prefix: props.prefix, filename: one.raw.name, key: uploadKey.value, keepFilename: props.keepFilename }))
  let uploadId = r.data
  one.uploading.uploadId = uploadId
  for (let i = 0; i < chunksList.length; i++) {
    try {
      await uploadChunk(one, chunksList, i, uploadId, 0)
    } catch (e) {
      one.error = true
      request.post(base + 'multipartUploadAbort?uploadId=' + one.uploading.uploadId)
      return;
    }
  }
  r = await request.post(base + 'multipartUploadComplete?uploadId=' + one.uploading.uploadId)
  if (r.code == 200) {
    delete one.raw
    delete one.uploading
    one.url = r.data
    if (props.max == 1) {
      mv.value = r.data
    } else {
      mv.value = data.value.filter(a => 'url' in a).map(a => a.url)
    }
  } else {
    one.error = true
    request.post(base + 'multipartUploadAbort?uploadId=' + one.uploading.uploadId)
    return;
  }
}

/**
 * 上传分片
 * @param index
 * @param retry
 */
const uploadChunk = async (one, chunksList, index, uploadId, retry = 0) => {
  if (retry >= props.retry) {
    throw new Error('上传分片失败')
  }
  retry++
  let formData = new FormData()
  formData.append('uploadId', uploadId)
  formData.append('file', chunksList[index])
  formData.append('partNumber', index + 1)
  let onUploadProgress = (event) => {
    one.uploading.loaded = props.chunksize * index + event.loaded
  }
  try {
    let r = await request.post(base + "multipartUpload?r=" + index + '' + Math.random(), formData, {
      onUploadProgress,
      timeout: 600000,
      showLoading: false,
      signal: one.uploading.controller.signal
    })
    if (r.code != 200) {
      await uploadChunk(one, chunksList, index, uploadId, retry)
    }
  } catch (e) {
    await uploadChunk(one, chunksList, index, uploadId, retry)
  }
}

/**
 * 分片
 * @param file 文件
 * @param chunksize 分片大小 
 */
const chunkFile = (file, chunksize) => {
  const chunks = Math.ceil(file.size / chunksize)
  const chunksList = []
  let currentChunk = 0
  while (currentChunk < chunks) {
    const start = currentChunk * chunksize;
    const end = Math.min(file.size, start + chunksize);
    const chunk = file.slice(start, end);
    chunksList.push(chunk);
    currentChunk++;
  }
  return chunksList;
}

defineExpose({ open, reloadUploadKey })
</script>
<style lang="scss" scoped>
.doing {
  opacity: .5;
  position: relative;

  &::before {
    content: '';
    position: absolute;
    height: 100%;
    width: 100%;
    background-color: #0000;
    cursor: not-allowed;

  }
}

.list {
  .item {
    display: flex;
    align-items: center;

    position: relative;
    overflow: hidden;
    border-radius: .3em;
    margin-top: .5em;
    padding: .5em;

    &.border {
      border: solid 1px #00000007;
    }

    &.abort {
      opacity: .5;
    }

    &.error {
      border-color: var(--el-color-danger, #f56c6c);
    }

    &>:nth-child(2) {
      cursor: pointer;

      &:hover {
        transform: translate(1px, 1px);
      }
    }

    &::before {
      content: '';
      pointer-events: none;
      background-color: #00000005;
      position: absolute;
      top: 0;
      left: 0;
      height: 100%;
      width: var(--ps, 0);
    }

    .btn {
      font-size: .7em;
      color: var(--el-color-danger, #f56c6c);
      margin-left: 1em;
      cursor: pointer;

      &:hover {
        transform: translate(1px, 1px);
      }
    }

  }
}


.icon {
  background-image: url(data:image/png;base64,[encoded data, 18268 characters]);
  background-repeat: no-repeat;
  background-position-y: 0;
  background-size: 140em 7em;
  width: 7em;
  height: 7em;
  background-position: -14em center;
  font-size: .4em;
  margin-right: .5em;
  flex-flow: 1;
  flex-shrink: 1;

  &.icon-dir {
    background-position: 0 center;
  }

  &.icon-zip {
    background-position: -7em center;
  }

  &.icon-filelist {
    background-position: -21em center;
  }

  &.icon-rar,
  &.icon-7z,
  &.icon-tar,
  &.icon-gz {
    background-position: -28em center;
  }

  &.icon-xls,
  &.icon-xlsx {
    background-position: -35em center;
  }

  &.icon-doc,
  &.icon-docx {
    background-position: -42em center;
  }

  &.icon-ppt,
  &.icon-pptx {
    background-position: -49em center;
  }

  &.icon-vsd {
    background-position: -56em center;
  }

  &.icon-pdf {
    background-position: -63em center;
  }

  &.icon-txt,
  &.icon-md,
  &.icon-json,
  &.icon-htm,
  &.icon-xml,
  &.icon-html,
  &.icon-js,
  &.icon-css,
  &.icon-php,
  &.icon-jsp,
  &.icon-asp {
    background-position: -70em center;
  }

  &.icon-apk {
    background-position: -77em center;
  }

  &.icon-exe {
    background-position: -84em center;
  }

  &.icon-ipa {
    background-position: -91em center;
  }

  &.icon-mp4,
  &.icon-mov,
  &.icon-mpeg,
  &.icon-rm,
  &.icon-rmvb {
    background-position: -98em center;
  }

  &.icon-wav,
  &.icon-wmv,
  &.icon-mid,
  &.icon-mp3 {
    background-position: -105em center;
  }

  &.icon-bmp,
  &.icon-jpg,
  &.icon-jpeg,
  &.icon-png,
  &.icon-webp,
  &.icon-psd,
  &.icon-tiff,
  &.icon-tif,
  &.icon-eps,
  &.icon-raw,
  &.icon-svg,
  &.icon-ai,
  &.icon-avif,
  &.icon-apng,
  &.icon-wmf,
  &.icon-pcd,
  &.icon-gif {
    background-position: -112em center;
  }

}
</style>