<template>
  <div class="h5ScanCode-container" :style="{ fontSize: realSize, zIndex }" v-if="visible">
    <div class="h5ScanCode-progress" />
    <div class="h5ScanCode-album" v-if="allowOpenAlbum" @click="openSelector">相册</div>
    <div class="h5ScanCode-torch" v-if="allowOpenTorch && torch.enabled" @click="toggleOpenTorch">
      {{ torch.open ? '关闭' : '开启' }}照明
    </div>
    <div class="h5ScanCode-message" :class="{ visible: message.visible }">{{ message.text }}</div>
    <div class="h5ScanCode-closeBtn" @click="closeInstance" />
  </div>
</template>

<script>
import jsQR from 'jsqr'
import { VIDEO_RATIO, CANVAS_SCALE } from '../config'

let instance = null

/**
 *  H5扫码
 *  @description 适用于H5环境的扫码Vue组件，需配置https环境。
 *  @param  {Object}    props.show            是否启动扫码 (支持 v-model 绑定)
 *  @param  {String}    props.scanType        扫码类型 ( 'qrCode': 默认值，二维码 )
 *  @param  {String}    props.facingMode      摄像头方向 ( 'back': 默认值，后置摄像头 | 'front': 前置摄像头 )
 *  @param  {Boolean}   props.allowOpenAlbum  是否允许从相册选择 ( 默认值: true )
 *  @param  {Boolean}   props.allowOpenTorch  是否允许开启闪光灯 ( 默认值: true )
 *  @param  {String}    props.fontSize        控件容器字体大小 ( 默认值: '28rpx'，默认单位: 'px'，单位 'rpx' 表示根据屏幕宽度自适应的像素单位 )
 *  @param  {Number}    props.zIndex          控件容器图层层级 ( 默认值: 999999 )
 *  @param  {Function}  on.success            扫码成功回调，回调参数: (e: { result: 结果值, scanType: 扫码类型 })
 *  @param  {Function}  on.fail               扫码失败回调，回调参数: (e: { cancel: 是否取消, ...错误信息 })
 *  @param  {Function}  on.complete           扫码完成回调，回调参数: (e: { success: 是否成功, ...成功/失败结果 })
 */
export default {
  name: 'h5-scan-code',
  props: {
    show: { type: Boolean, default: false },
    scanType: { type: String, default: 'qrCode' },
    facingMode: { type: String, default: 'back' },
    allowOpenAlbum: { type: Boolean, default: true },
    allowOpenTorch: { type: Boolean, default: true },
    fontSize: { type: String, default: '28rpx' },
    zIndex: { type: Number, default: 999999 }
  },
  model: {
    prop: 'show',
    event: 'change'
  },
  data() {
    return {
      visible: false,
      width: 0,
      height: 0,
      video: null,
      canvas: null,
      stream: null,
      selector: null,
      torch: {
        enabled: false,
        open: false
      },
      message: {
        visible: false,
        text: ''
      }
    }
  },
  computed: {
    realSize() {
      const size = this.fontSize
      if (size > 0) return `${+size}px`
      if (/(r|u)px$/.test(size)) return `${this.width / 750 * parseFloat(size) || 0}px`
      return size || 0
    }
  },
  watch: {
    show: {
      immediate: true,
      handler(val) { val && this.init() }
    }
  },
  methods: {
    async init() {
      try {
        // 停止原实例
        if (instance) instance.stopInstance()

        // 设置配置项
        instance = this
        this.width = document.documentElement.clientWidth
        this.height = document.documentElement.clientHeight

        // 检测是否支持
        if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
          throw new DOMException('Not supported to open the user media', 'NotSupportedError')
        }

        // 设置视频流
        this.stream = await navigator.mediaDevices.getUserMedia({
          audio: false,
          video: {
            width: this.height * VIDEO_RATIO,
            height: this.width * VIDEO_RATIO,
            facingMode: { exact: this.facingMode == 'front' ? 'user' : 'environment' }
          }
        })
        this.stream.track = this.stream.getVideoTracks()[0]

        // 显示容器
        this.visible = true
        await this.$nextTick()

        // 设置视频元素
        this.video = document.createElement('video')
        this.video.className = 'h5ScanCode-video'
        this.video.width = this.width
        this.video.height = this.height
        this.video.srcObject = this.stream
        this.video.controls = false
        this.video.playsInline = true
        this.$el.insertBefore(this.video, this.$el.firstChild)
        
        // 设置相册选择器元素
        this.selector = document.createElement('input')
        this.selector.type = 'file'
        this.selector.accept = 'image/*'
        this.selector.addEventListener('change', this.parseSelectFile)

        // 设置闪光灯开关属性
        this.allowOpenTorch && setTimeout(() => {
          this.torch.enabled = !!this.stream.track.getCapabilities().torch
        }, 500)

        // 设置扫描画布元素
        this.canvas = document.createElement('canvas')
        this.canvas.context2d = this.canvas.getContext('2d')
        this.canvas.width = this.width * CANVAS_SCALE * VIDEO_RATIO
        this.canvas.height = this.height * CANVAS_SCALE * VIDEO_RATIO

        // 开始扫描
        document.body.appendChild(this.$el)
        this.video.play()
        this.requestTick()
      } catch (e) {
        this.toComplete(e, false)
      }
    },
    requestTick() {
      if (!this.visible) return
      if (this.video.readyState === this.video.HAVE_ENOUGH_DATA) {
        this.canvas.context2d.drawImage(
          this.video,
          this.width * (1 - CANVAS_SCALE) * VIDEO_RATIO / 2,
          this.height * (1 - CANVAS_SCALE) * VIDEO_RATIO / 2,
          this.width * CANVAS_SCALE * VIDEO_RATIO,
          this.height * CANVAS_SCALE * VIDEO_RATIO,
          0, 0,
          this.width * CANVAS_SCALE * VIDEO_RATIO,
          this.height * CANVAS_SCALE * VIDEO_RATIO
        )
        const imageData = this.canvas.context2d.getImageData(
          0, 0,
          this.width * CANVAS_SCALE * VIDEO_RATIO,
          this.height * CANVAS_SCALE * VIDEO_RATIO
        )
        const code = jsQR(imageData.data, imageData.width, imageData.height, {
          inversionAttempts: 'dontInvert'
        })
        if (code && code.data) {
          this.toComplete({ result: code.data, scanType: this.scanType }, true)
          return
        }
      }
      requestAnimationFrame(this.requestTick)
    },
    openSelector() {
      this.selector.value = ''
      this.selector.click()
    },
    async parseSelectFile() {
      const file = this.selector && this.selector.files[0] || void 0
      if (!file) return
      try {
        const url = URL.createObjectURL(file)
        const image = new Image()
        await new Promise((resolve, reject) => {
          image.onload = () => resolve(image)
          image.onerror = reject
          image.src = url
        })
        URL.revokeObjectURL(url)
        const canvas = document.createElement('canvas')
        const context = canvas.getContext('2d')
        canvas.width = image.width, canvas.height = image.height
        context.drawImage(image, 0, 0, image.width, image.height)
        const imageData = context.getImageData(0, 0, image.width, image.height)
        const code = jsQR(imageData.data, imageData.width, imageData.height, {
          inversionAttempts: 'attemptBoth'
        })
        code && code.data
          ? this.toComplete({ result: code.data, scanType: this.scanType }, true)
          : this.showMessage('未识别到内容')
      } catch (e) {
        this.showMessage('识别内容失败')
      }
    },
    toggleOpenTorch() {
      const open = !this.torch.open
      this.stream.track.applyConstraints({ advanced: [{ torch: open }] })
      this.torch.open = open
    },
    async showMessage(message) {
      const wait = (delay) => new Promise(resolve => {
        clearTimeout(this.message.keyframe)
        this.message.keyframe = setTimeout(resolve, delay)
      })
      this.message.text = message
      this.message.visible = true
      await wait(3500)
      this.message.visible = false
      await wait(500)
      this.message.text = ''
    },
    closeInstance() {
      this.toComplete({ cancel: true }, false)
    },
    stopInstance() {
      try {
        this.stream && this.stream.getTracks().forEach(track => track.stop())
        this.$el && this.$el.remove()
      } catch (e) {} finally {
        this.visible = false
        this.video = null
        this.canvas = null
        this.stream = null
        this.selector = null
        this.torch.enabled = false
        this.torch.open = false
        this.message.visible = false
        this.message.text = ''
      }
    },
    toComplete(e, success = false) {
      this.stopInstance(), instance = null, e.success = success
      this.$emit('change', false)
      this.$emit(success ? 'success' : 'fail', e)
      this.$emit('complete', e)
    }
  }
}
</script>

<style src="../style.css"></style>
