<!-- @format -->

<template>
  <div
    id="slideVerify"
    class="slide-verify"
    :style="{ width: w + 'px' }"
    onselectstart="return false;">
    <!-- 图片加载遮蔽罩 -->
    <div :class="{ 'slider-verify-loading': loadBlock }"></div>
    <canvas ref="canvas" :width="w" :height="h"></canvas>
    <div
      v-if="show"
      class="slide-verify-refresh-icon"
      @click="refresh"
      :style="{ pointerEvents: disabled ? 'none' : '' }">
      <ns-icon name="refreash" size="20" />
    </div>
    <canvas ref="block" :width="w" :height="h" class="slide-verify-block"></canvas>

    <div
      class="slide-verify-slider"
      :class="{
        'container-active': containerCls.containerActive,
        'container-success': containerCls.containerSuccess,
        'container-fail': containerCls.containerFail,
      }">
      <div class="slide-verify-slider-mask" :style="{ width: sliderBox.width }">
        <!-- slider -->
        <div
          class="slide-verify-slider-mask-item"
          :style="{ left: sliderBox.left }"
          @mousedown="sliderDown"
          @touchstart="touchStartEvent"
          @touchmove="touchMoveEvent"
          @touchend="touchEndEvent">
          <i
            :class="[
              'slide-verify-slider-mask-item-icon',
              'iconfont',
              `icon-${sliderBox.iconCls}`,
            ]"></i>
        </div>
      </div>
      <span class="slide-verify-slider-text">{{ sliderText }}</span>
    </div>
  </div>
</template>

<script lang="ts">
  interface frontPictureClass {
    width: Number;
    height: Number;
    src: 'string';
  }

  import { defineComponent, reactive, ref, onMounted, PropType, onBeforeUnmount, watch } from 'vue';

  import { useSlideAction } from './hooks';
  import { createImg, draw, getRandomImg, getRandomNumberByRange, throttle } from './util';

  export default defineComponent({
    name: 'SlideVerify',
    props: {
      // block length
      l: {
        type: Number,
        default: 42,
      },
      // block radius
      r: {
        type: Number,
        default: 10,
      },
      // canvas width
      w: {
        type: Number,
        default: 291,
      },
      // canvas height
      h: {
        type: Number,
        default: 360,
      },
      sliderText: {
        type: String,
        default: 'Slide filled right',
      },
      accuracy: {
        type: Number,
        default: 5, // 若为 -1 则不进行机器判断
      },
      show: {
        type: Boolean,
        default: true,
      },
      //是否是后端异步验证，而不是纯前端的机器人验证
      asyncVerify: {
        type: Boolean,
        default: true,
      },
      imgs: {
        type: Array as PropType<any[]>,
        default: () => [],
      },
      frontPicture: {
        type: Object as PropType<frontPictureClass>,
        default: () => {
          return {
            src: '',
            width: 0,
            height: 0,
          };
        },
      },
      interval: {
        // 节流时长间隔
        type: Number,
        default: 50,
      },
    },
    emits: ['success', 'again', 'fail', 'refresh', 'verify'],
    setup(props, { emit }) {
      let { imgs, l, r, accuracy, interval, asyncVerify, frontPicture } = props;
      // 图片加载完关闭遮蔽罩
      const loadBlock = ref(true);
      const blockX = ref(0);
      const blockY = ref(0);
      let w = ref(props.w);
      let h = ref(props.h);
      let disabled = ref(false);
      // class
      const containerCls = reactive({
        containerActive: false, // container active class
        containerSuccess: false, // container success class
        containerFail: false, // container fail class
      });
      // sliderMaskWidth sliderLeft
      const sliderBox = reactive({
        iconCls: 'arrow-right',
        width: '0',
        left: '0',
      });

      const block = ref<HTMLCanvasElement>();
      const blockCtx = ref<CanvasRenderingContext2D | null>();
      const canvas = ref<HTMLCanvasElement>();
      const canvasCtx = ref<CanvasRenderingContext2D | null>();
      let img: HTMLImageElement;
      const { success, start, move, end, verify } = useSlideAction();
      const dealCTx = (canvas, context) => {
        // context = canvas.getContext('2d');
        // 2、获取像素比，放大比例为：devicePixelRatio / webkitBackingStorePixelRatio , 以下是兼容的写法
        let backingStore =
          context.backingStorePixelRatio ||
          context.webkitBackingStorePixelRatio ||
          context.mozBackingStorePixelRatio ||
          context.msBackingStorePixelRatio ||
          context.oBackingStorePixelRatio ||
          context.backingStorePixelRatio ||
          1;
        let ratio = (window.devicePixelRatio || 1) / backingStore;
        // 3、将 Canvas 宽高进行放大，要设置canvas的画布大小，使用的是 canvas.width 和 canvas.height；
        let oldWidth = canvas.width;
        let oldHeight = canvas.height;
        canvas.width = oldWidth * ratio;
        canvas.height = oldHeight * ratio;
        // 4、要设置画布的实际渲染大小，使用的 style 属性或CSS设置的 width 和 height，只是简单的对画布进行缩放。
        canvas.style.width = oldWidth + 'px';
        canvas.style.height = oldHeight + 'px';
        // 5、放大倍数：由于 Canvas 放大后，相应的绘制图片时也要放大，可以直接使用 scale 方法
        context.scale(ratio, ratio);
      };
      // event
      const reset = () => {
        success.value = false;
        containerCls.containerActive = false;
        containerCls.containerSuccess = false;
        containerCls.containerFail = false;
        sliderBox.iconCls = 'arrow-right';
        sliderBox.left = '0';
        sliderBox.width = '0';

        block.value!.style.left = '0';

        canvasCtx.value?.clearRect(0, 0, w.value, h.value);
        blockCtx.value?.clearRect(0, 0, w.value, h.value);

        if (!asyncVerify) {
          block.value!.width = w.value;
        }

        // generate img
        img.src = getRandomImg(imgs);
      };
      const refresh = () => {
        reset();
        emit('refresh');
      };

      const successFun = (name: string, timestamp?: number) => {
        if (name === 'success') {
          containerCls.containerSuccess = true;
          sliderBox.iconCls = 'success';
          success.value = true;
          emit('success', timestamp);
        }
        if (name === 'again') {
          containerCls.containerFail = true;
          sliderBox.iconCls = 'fail';
          emit('again');
        }
        if (name === 'fail') {
          containerCls.containerFail = true;
          sliderBox.iconCls = 'fail';
          emit('fail');
          setTimeout(() => {
            reset();
          }, 1000);
        }
      };

      function moveCb(moveX: number) {
        sliderBox.left = moveX - 5 + 'px';
        let blockLeft = ((w.value - 40 - 20) / (w.value - 40)) * moveX;
        block.value!.style.left = blockLeft + 'px';
        containerCls.containerActive = true;
        sliderBox.width = moveX + 'px';
      }

      function endCb(timestamp: number) {
        const { spliced, TuringTest } = verify(block.value!.style.left, blockX.value, accuracy);
        //不做自动校验，改为手动校验规则
        if (asyncVerify) {
          emit('verify', block.value!.style.left, timestamp);
          return;
        }
        if (spliced) {
          if (accuracy === -1) {
            successFun('success', timestamp);
            return;
          }
          if (TuringTest) {
            // success
            successFun('success', timestamp);
          } else {
            successFun('again', timestamp);
          }
        } else {
          successFun('fail', timestamp);
        }
      }

      const touchMoveEvent = throttle((e: TouchEvent | MouseEvent) => {
        move(w.value, e, moveCb);
      }, interval);

      const touchEndEvent = (e: TouchEvent | MouseEvent) => {
        end(e, endCb);
      };

      const setwatchFUn = (time?: number) => {
        !time ? (time = 100) : '';
        return new Promise((resolve: Function) => {
          setTimeout(() => {
            resolve();
          }, time);
        });
      };

      const resetImg = async () => {
        const _canvasCtx = canvas.value?.getContext('2d');
        const _blockCtx = block.value?.getContext('2d');
        canvasCtx.value = _canvasCtx;
        blockCtx.value = _blockCtx;

        await setwatchFUn(300);
        if (!imgs.length) {
          resetImg();
          return;
        }

        if (asyncVerify) {
          img = createImg(imgs, () => {
            if (_canvasCtx) {
              _canvasCtx.drawImage(img, 0, 0, props.w, props.h);
            }
          });

          setTimeout(() => {
            let frontImg = createImg([frontPicture.src], () => {
              if (_blockCtx) {
                _blockCtx.drawImage(frontImg, 0, 0, frontPicture.width, frontPicture.height);
              }
              disabled.value = false;
            });
          }, 500);
        } else {
          img = createImg(imgs, () => {
            loadBlock.value = false;
            const L = l + r * 2 + 3;
            // draw block
            blockX.value = getRandomNumberByRange(L + 10, w.value - (L + 10));
            blockY.value = getRandomNumberByRange(10 + r * 2, h.value - (L + 10));
            if (_canvasCtx && _blockCtx) {
              draw(_canvasCtx, blockX.value, blockY.value, l, r, 'fill');
              draw(_blockCtx, blockX.value, blockY.value, l, r, 'clip');
              // draw image
              _canvasCtx.drawImage(img, 0, 0, w.value, h.value);
              _blockCtx.drawImage(img, 0, 0, w.value, h.value);
              // getImage
              const _y = blockY.value - r * 2 - 1;
              const imgData = _blockCtx.getImageData(blockX.value, _y, L, L);
              block.value!.width = L;
              _blockCtx.putImageData(imgData, 0, _y);
            }
            disabled.value = false;
          });
        }

        // bindEvent
        document.addEventListener('mousemove', touchMoveEvent);
        document.addEventListener('mouseup', touchEndEvent);
      };

      onMounted(async () => {
        resetImg();
      });

      // 移除全局事件
      onBeforeUnmount(() => {
        document.removeEventListener('mousemove', touchMoveEvent);
        document.removeEventListener('mouseup', touchEndEvent);
      });

      watch(
        () => props.imgs,
        (val: any[]) => {
          imgs = val;
        },
      );
      watch(
        () => props.frontPicture,
        (val: any) => {
          frontPicture = val;
        },
      );
      watch(
        () => props.w,
        (val: any) => {
          w.value = val;
        },
      );
      watch(
        () => props.h,
        (val: any) => {
          h.value = val;
        },
      );

      return {
        block,
        canvas,
        loadBlock,
        containerCls,
        sliderBox,
        refresh,
        sliderDown: start,
        touchStartEvent: start,
        touchMoveEvent,
        touchEndEvent,
        successFun,
        resetImg,
        disabled,
      };
    },
  });
</script>

<style scoped lang="less">
  @import url('../assets/iconfont.css');

  .slide-verify-refresh-icon {
    z-index: 10;
    display: flex;
    align-items: center;
    justify-content: space-around;
    width: 32px;
    height: 32px;
    background-color: rgba(0, 0, 0, 0.2);
  }

  .slide-verify-slider-mask-item-icon {
    color: rgba(0, 0, 0, 0.4) !important;
  }

  // .slide-verify-slider-mask-item:hover {
  //   color: #fff !important;
  // }

  .container-success .slide-verify-slider-mask .iconfont {
    color: #fff !important;
  }

  .container-fail .slide-verify-slider-mask .iconfont {
    color: #fff !important;
  }

  // .container-active .slide-verify-slider-mask {
  //   background-color: #1991fa;
  // }
  // .container-active .slide-verify-slider-mask .iconfont {
  //   color: #fff !important;
  // }

  .slide-verify-slider-mask-item {
    width: 38px !important;
    height: 38px !important;
    border-radius: 2px;
  }

  .slide-verify-slider {
    border-radius: 2px;
  }

  .slide-verify-refresh-icon .iconfont {
    font-size: 25px !important;
    transform: rotate(-30deg);
  }

  .position() {
    position: absolute;
    left: 0;
    top: 0;
  }
  .slide-verify {
    position: relative;
    &-loading {
      .position();
      right: 0;
      bottom: 0;
      background: rgba(255, 255, 255, 0.9);
      z-index: 999;
      animation: loading 1.5s infinite;
    }

    &-block {
      .position();
    }

    &-refresh-icon {
      position: absolute;
      right: 0;
      top: 0;
      width: 32px;
      height: 32px;
      cursor: pointer;
      .iconfont {
        font-size: 34px;
        color: #fff;
      }
    }
    &-slider {
      position: relative;
      text-align: center;
      width: 100%;
      height: 40px;
      line-height: 40px;
      margin-top: 15px;
      background: #f7f9fa;
      color: #45494c;
      border: 1px solid #e4e7eb;
      &-mask {
        .position();
        height: 40px;
        border: 0 solid #1991fa;
        background: #d1e9fe;
        &-item {
          .position();
          width: 40px;
          height: 40px;
          background: #fff;
          box-shadow: 0 0 3px rgba(0, 0, 0, 0.3);
          cursor: pointer;
          transition: background 0.2s linear;
          display: flex;
          align-items: center;
          justify-content: center;
          &-icon {
            line-height: 1;
            font-size: 30px;
            color: #303030;
          }
        }
      }
    }
  }

  .container-active .slide-verify-slider-mask {
    height: 38px;
    border-width: 1px;
    &-item {
      height: 38px;
      top: -1px;
      border: 1px solid #1991fa;
    }
  }

  .container-success .slide-verify-slider-mask {
    height: 38px;
    border: 1px solid #52ccba;
    background-color: #d2f4ef;
    &-item {
      height: 38px;
      top: -1px;
      border: 1px solid #52ccba;
      background-color: #52ccba !important;
    }
    .iconfont {
      color: #fff;
    }
  }

  .container-fail .slide-verify-slider-mask {
    height: 38px;
    border: 1px solid #f57a7a;
    background-color: #fce1e1;
    &-item {
      height: 38px;
      top: -1px;
      border: 1px solid #f57a7a;
      background-color: #f57a7a !important;
    }
    .iconfont {
      color: #fff;
    }
  }

  .container-active .slide-verify-slider-text,
  .container-success .slide-verify-slider-text,
  .container-fail .slide-verify-slider-text {
    display: none;
  }

  @keyframes loading {
    0% {
      opacity: 0.7;
    }
    100% {
      opacity: 9;
    }
  }
</style>
