<script lang='ts' setup>
import { chevronBackOutline, images } from "ionicons/icons";
import jsQR from "jsqr";

const emit = defineEmits<{
  success: [value: string];
  error: [error: Error];
  close: [];
}>();

const videoInst = useTemplateRef<HTMLVideoElement>("videoInst");
const canvasInst = useTemplateRef<HTMLCanvasElement>("canvasInst");
const fileInputInst = useTemplateRef<HTMLInputElement>("fileInputInst");

const scanning = ref(true);
let stream: MediaStream | null = null;

async function start() {
  if (!navigator.mediaDevices || !navigator.mediaDevices.getUserMedia) {
    const error = new Error("getUserMedia not supported on your browser!");
    console.error(error);
    emit("error", error);
    return;
  }
  if (!videoInst.value) {
    const error = new Error("video element not found!");
    console.error(error);
    emit("error", error);
    return;
  }

  try {
    stream = await navigator.mediaDevices.getUserMedia({
      audio: false,
      video: { facingMode: "environment", width: 512, height: 512 },
    });
    videoInst.value.srcObject = stream;
    videoInst.value.setAttribute("playsinline", "true"); // required to tell iOS safari we don't want fullscreen
    await videoInst.value.play();
    scanning.value = true;
    scanFrame();
  }
  catch (error) {
    console.error("Failed to start camera:", error);
  }
}

function scanFrame() {
  if (!scanning.value || !videoInst.value || !canvasInst.value) {
    return;
  }

  const video = videoInst.value;
  const canvas = canvasInst.value;
  const ctx = canvas.getContext("2d");

  if (!ctx)
    return;

  if (video.readyState !== video.HAVE_ENOUGH_DATA) {
    requestAnimationFrame(scanFrame);
    return;
  }

  canvas.width = video.videoWidth;
  canvas.height = video.videoHeight;

  ctx.drawImage(video, 0, 0, canvas.width, canvas.height);

  const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

  const code = jsQR(imageData.data, imageData.width, imageData.height, {
    inversionAttempts: "dontInvert",
  });

  if (code) {
    scanning.value = false;
    emit("success", code.data);
    stop();
  }
  else {
    requestAnimationFrame(scanFrame);
  }
}

function stop() {
  scanning.value = false;
  if (stream) {
    stream.getTracks().forEach(track => track.stop());
    stream = null;
  }
  if (videoInst.value) {
    videoInst.value.srcObject = null;
  }
}

function handleSelectImage() {
  fileInputInst.value?.click();
}

async function handleFileChange(event: Event) {
  const target = event.target as HTMLInputElement;
  const file = target.files?.[0];
  if (!file) {
    return;
  }

  try {
    const image = new Image();
    const canvas = canvasInst.value;
    if (!canvas) {
      return;
    }

    const ctx = canvas.getContext("2d");
    if (!ctx) {
      return;
    }

    // 读取图片
    const reader = new FileReader();
    reader.onload = (e) => {
      image.onload = () => {
        canvas.width = image.width;
        canvas.height = image.height;
        ctx.drawImage(image, 0, 0);
        const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
        const code = jsQR(imageData.data, imageData.width, imageData.height, {
          inversionAttempts: "dontInvert",
        });

        if (code) {
          emit("success", code.data);
          stop();
        }
        else {
          const error = new Error("未识别到二维码");
          emit("error", error);
        }
      };
      image.src = e.target?.result as string;
    };
    reader.readAsDataURL(file);
  }
  catch (error) {
    console.error("Failed to scan image:", error);
    emit("error", error as Error);
  }
  finally {
    if (target) {
      target.value = "";
    }
  }
}

onMounted(() => {
  start();
});

onUnmounted(() => {
  stop();
});

defineExpose({
  start,
  stop,
});
</script>

<template>
  <div class="relative h-full w-full overflow-hidden bg-black">
    <video ref="videoInst" class="h-full w-full object-cover" />
    <canvas ref="canvasInst" class="hidden" />
    <input
      ref="fileInputInst"
      type="file"
      accept="image/*"
      class="hidden"
      @change="handleFileChange"
    >

    <div class="absolute left-0 top-0 z-10 p-4 pt-[calc(1rem+var(--ion-safe-area-top,0px))] pl-[calc(1rem+var(--ion-safe-area-left,0px))]">
      <button class="z-1 flex items-center" @click="emit('close')">
        <ion-icon :icon="chevronBackOutline" class="text-2xl text-white" />
      </button>
    </div>

    <div class="absolute inset-0 flex flex-col items-center justify-center bg-black/50">
      <div class="qr-scanner-frame relative size-70 bg-transparent">
        <div class="absolute left-0 top-0 size-8 rounded-tl border-3 border-white border-b-0 border-r-0" />
        <div class="absolute right-0 top-0 size-8 rounded-tr border-3 border-white border-b-0 border-l-0" />
        <div class="absolute bottom-0 left-0 size-8 rounded-bl border-3 border-white border-r-0 border-t-0" />
        <div class="absolute bottom-0 right-0 size-8 rounded-br border-3 border-white border-l-0 border-t-0" />

        <div
          v-if="scanning"
          class="qr-scanner-line absolute inset-x-0 top-0 h-0.5 animate-scan bg-linear-to-r from-transparent via-white to-transparent shadow-[0_0_8px_rgba(255,255,255,0.8)]"
        />
      </div>

      <div class="mt-10 rounded-lg bg-black/60 px-6 py-3 text-sm text-white backdrop-blur">
        将二维码放入框内，即可自动扫描
      </div>

      <div class="mt-6">
        <ion-button color="light" @click="handleSelectImage">
          <ion-icon slot="start" :icon="images" />
        </ion-button>
      </div>
    </div>
  </div>
</template>

<style lang='css' scoped>
.qr-scanner-frame {
  box-shadow: 0 0 0 9999px rgba(0, 0, 0, 0.5);
}

@keyframes scan {
  0% {
    transform: translateY(0);
  }
  100% {
    transform: translateY(280px);
  }
}

.animate-scan {
  animation: scan 2s linear infinite;
}
</style>
