<template>
  <div v-if="visible" class="image-cropper-mask">
    <div class="cropper-box">
      <vue-cropper
        ref="cropperRef"
        :img="imgUrl"
        :output-size="option.size"
        :output-type="option.outputType"
        :info="true"
        :full="option.full"
        :can-move="option.canMove"
        :can-move-box="option.canMoveBox"
        :original="option.original"
        :auto-crop="option.autoCrop"
        :fixed="option.fixed"
        :fixed-number="option.fixedNumber"
        :center-box="option.centerBox"
        :info-true="option.infoTrue"
        :fixed-box="option.fixedBox"
        :mode="option.mode"
        :auto-crop-width="option.autoCropWidth"
        :auto-crop-height="option.autoCropHeight"
        @img-load="imgLoad"
      ></vue-cropper>
      <div v-if="imgLoading" class="loading-mask">
        <Icon name="global_loading" loading size="96" />
      </div>
    </div>
    <div class="tools">
      <mj-button @click="rotateLeft">左旋转</mj-button>
      <mj-button @click="rotateRight">右旋转</mj-button>
      <mj-button type="warn" @click="handleCancel">取消</mj-button>
      <mj-button type="primary" :loading="loading" @click="handleConfirm">确认</mj-button>
    </div>
  </div>
</template>

<script setup lang="ts">
import { ref, reactive, watch, nextTick } from 'vue'
import 'vue-cropper/dist/index.css'
import { VueCropper } from 'vue-cropper'

const props = defineProps<{
  visible: boolean
  img: string | Blob | File
}>()

const emit = defineEmits(['update:visible', 'confirm', 'cancel'])

const cropperRef = ref()
const imgUrl = ref('')
const loading = ref(false)
const imgLoading = ref(true)

// 平板/低端设备建议较小尺寸，减小处理与上传耗时
const MAX_IMG_DIMENSION = 1280

const option = reactive({
  size: 0.85,
  full: false,
  outputType: 'jpeg',
  canMove: true,
  original: false,
  canMoveBox: true,
  autoCrop: true,
  autoCropWidth: 0,
  autoCropHeight: 0,
  centerBox: false,
  high: true,
  maxImgSize: MAX_IMG_DIMENSION,
  fixed: false,
  fixedNumber: [1, 1],
  infoTrue: false,
  fixedBox: false,
  mode: 'contain',
})

// 用于校验异步过程中 props.img 是否被替换，避免设置过时数据
let loadToken = 0

function readFileAsDataURL(file: Blob | File): Promise<string> {
  return new Promise((resolve, reject) => {
    const reader = new FileReader()
    reader.onload = e => resolve((e.target?.result as string) || '')
    reader.onerror = reject
    reader.readAsDataURL(file)
  })
}

// 优先使用 createImageBitmap（异步解码、更快），不可用时退回 Image 加载
async function loadImageSize(
  source: Blob | File | string,
): Promise<{ bitmap?: ImageBitmap; img?: HTMLImageElement; w: number; h: number }> {
  // File 继承自 Blob，因此只需判断 Blob 即可同时覆盖 File
  if (typeof createImageBitmap === 'function' && typeof source !== 'string') {
    try {
      const bitmap = await createImageBitmap(source)
      return { bitmap, w: bitmap.width, h: bitmap.height }
    } catch {
      /* fallthrough */
    }
  }
  return new Promise((resolve, reject) => {
    const img = new Image()
    img.onload = () => resolve({ img, w: img.naturalWidth, h: img.naturalHeight })
    img.onerror = reject
    if (typeof source === 'string') {
      img.src = source
    } else {
      const reader = new FileReader()
      reader.onload = e => {
        img.src = (e.target?.result as string) || ''
      }
      reader.onerror = reject
      reader.readAsDataURL(source)
    }
  })
}

async function downscaleImage(source: Blob | File | string): Promise<string> {
  try {
    const { bitmap, img, w, h } = await loadImageSize(source)
    const drawSource = (bitmap || img) as CanvasImageSource
    const needScale = w > MAX_IMG_DIMENSION || h > MAX_IMG_DIMENSION
    const scale = needScale ? Math.min(MAX_IMG_DIMENSION / w, MAX_IMG_DIMENSION / h) : 1
    const canvas = document.createElement('canvas')
    canvas.width = Math.max(1, Math.round(w * scale))
    canvas.height = Math.max(1, Math.round(h * scale))
    const ctx = canvas.getContext('2d')!
    ctx.drawImage(drawSource, 0, 0, canvas.width, canvas.height)
    bitmap?.close?.()
    return canvas.toDataURL('image/jpeg', 0.9)
  } catch (e) {
    console.error('downscaleImage error', e)
    if (typeof source === 'string') return source
    try {
      return await readFileAsDataURL(source)
    } catch {
      return ''
    }
  }
}

watch(
  () => props.img,
  async (val: string | Blob | File | undefined) => {
    // 立即清空旧图，避免在异步处理新图时显示上一张图
    imgUrl.value = ''
    imgLoading.value = true
    if (!val) return

    const myToken = ++loadToken
    try {
      const url = await downscaleImage(val)
      // 防止后来的图片切换被覆盖
      if (myToken !== loadToken) return
      imgUrl.value = url
    } catch (e) {
      console.error('cropper image load error', e)
      if (myToken === loadToken) imgLoading.value = false
    }
  },
  { immediate: true },
)

function imgLoad(msg: string) {
  if (msg === 'success') {
    imgLoading.value = false
    nextTick(() => {
      if (cropperRef.value) {
        const { trueWidth, trueHeight, scale } = cropperRef.value
        option.autoCropWidth = trueWidth * scale
        option.autoCropHeight = trueHeight * scale
        option.centerBox = true
        nextTick(() => {
          cropperRef.value.goAutoCrop()
        })
      }
    })
  }
}

function rotateLeft() {
  cropperRef.value.rotateLeft()
}

function rotateRight() {
  cropperRef.value.rotateRight()
}

function handleCancel() {
  emit('update:visible', false)
  emit('cancel')
}

function handleConfirm() {
  loading.value = true
  cropperRef.value.getCropBlob((data: Blob) => {
    loading.value = false
    emit('confirm', data)
    emit('update:visible', false)
  })
}
</script>

<style scoped lang="scss">
.image-cropper-mask {
  position: fixed;
  top: 0;
  left: 0;
  z-index: 10001; // higher than hand-drawn-mask
  width: 100vw;
  height: 100%;
  background-color: rgba(0, 0, 0, 0.45);
  backdrop-filter: blur(4px);
  background: #f7faf9;
  display: flex;
  flex-direction: column;

  .cropper-box {
    flex: 1;
    width: 100%;
    height: calc(100% - 80px);
    background-image: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQAQMAAAAlPW0iAAAAA3NCSVQICAjb4U/gAAAABlBMVEXMzMz////TjRV2AAAACXBIWXMAAArrAAAC6wGCdbt9AAAAIKFInOMAAAABYktHRACIBR1IAAAAC0lEQVR4nGNgwA0AAJoAAW7mSAAAAAAASUVORK5CYII=');
    position: relative;

    .loading-mask {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      background: rgba(0, 0, 0, 0.3);
      display: flex;
      justify-content: center;
      align-items: center;
      z-index: 10002;
    }

    :deep(.crop-point) {
      width: 20px !important;
      height: 20px !important;
      background-color: #39f !important;
      border-radius: 50%;
      opacity: 0.8;
    }
  }

  .tools {
    height: 80px;
    display: flex;
    justify-content: flex-end;
    align-items: center;
    padding: 0 20px;
    background: #fff;
    .mj-button {
      margin: 0 4px;
    }
  }
}
</style>
