<script setup>
import {
  ref,
  watch,
  onMounted,
  onBeforeUnmount,
  computed,
  nextTick,
  getCurrentInstance,
} from "vue";
import PointSwitcher from "./PointSwitcher.vue";
import BowShotEffect from "@/components/BowShotEffect.vue";

import { MESSAGETYPES, MESSAGETYPESV2 } from "@/constants";
import { simulShootAPI, laserAimAPI, laserCloseAPI } from "@/apis";
import useStore from "@/store";
import { storeToRefs } from "pinia";
const store = useStore();
const { user, device } = storeToRefs(store);

const props = defineProps({
  currentRound: {
    type: Number,
    default: 0,
  },
  totalRound: {
    type: Number,
    default: 0,
  },
  scores: {
    type: Array,
    default: () => [],
  },
  blueScores: {
    type: Array,
    default: () => [],
  },
  redTeam: {
    type: Array,
    default: () => [],
  },
  blueTeam: {
    type: Array,
    default: () => [],
  },
  latestShotFlash: {
    type: Object,
    default: null,
  },
  mode: {
    type: String,
    default: "solo", // solo 单排，team 双排
  },
  stop: {
    type: Boolean,
    default: false,
  },
  targetRadius: {
    type: Number,
    default: 20,
  },
  hitRadiusPx: {
    type: Number,
    default: 2,
  },
  zoomHitRadiusPx: {
    type: Number,
    default: 5,
  },
  stableShotEffect: {
    type: Boolean,
    default: false,
  },
});

const pMode = ref(true);
const latestOne = ref(null);
const bluelatestOne = ref(null);
const timer = ref(null);
const dirTimer = ref(null);
const angle = ref(null);
const circleColor = ref("");
const shotEffect = ref(null);
const pendingShotEffect = ref(null);
const hiddenRedLatestKey = ref("");
const hiddenBlueLatestKey = ref("");
const targetShaking = ref(false);
const targetRect = ref({ left: 0, top: 0, width: 0, height: 0 });
const shakeTimer = ref(null);
const instance = getCurrentInstance();
let shotEffectRequestGeneration = 0;
const ROUND_TIP_OFFSET_Y = -32;
const EXPERIENCE_TIP_OFFSET_Y = -68;

function buildShotEffectKey(team, shot, fallbackKey = "") {
  return (
    fallbackKey ||
    [
      team,
      shot?.playerId ?? "",
      shot?.x ?? "",
      shot?.y ?? "",
      shot?.ring ?? "",
      shot?.ringX ? 1 : 0,
    ].join("-")
  );
}

function findShotPlayer(shot, team) {
  const players = team === "red" ? props.redTeam : props.blueTeam;
  return players.find((player) => String(player?.id) === String(shot?.playerId));
}

function isSvipShot(shot, team) {
  return findShotPlayer(shot, team)?.sVip === true;
}

function shouldPlayShotEffect(shot, team) {
  return !!shot && Number(shot.ring) > 0 && isSvipShot(shot, team);
}

function clearTipTimer() {
  if (timer.value) clearTimeout(timer.value);
  timer.value = null;
}

function showShotTip(team, shootData) {
  clearTipTimer();

  if (team === "red") {
    latestOne.value = shootData;
    timer.value = setTimeout(() => {
      latestOne.value = null;
      timer.value = null;
    }, 1000);
    return;
  }

  bluelatestOne.value = shootData;
  timer.value = setTimeout(() => {
    bluelatestOne.value = null;
    timer.value = null;
  }, 1000);
}

function triggerShotEffect(team, shot, fallbackKey = "", viewportMode = false) {
  const key = buildShotEffectKey(team, shot, fallbackKey);

  if (shotEffect.value?.team === "red") hiddenRedLatestKey.value = "";
  if (shotEffect.value?.team === "blue") hiddenBlueLatestKey.value = "";

  if (team === "red") {
    latestOne.value = null;
    hiddenRedLatestKey.value = key;
  } else {
    bluelatestOne.value = null;
    hiddenBlueLatestKey.value = key;
  }

  shotEffect.value = { key, team, shot, viewportMode };
}

async function prepareShotEffect(team, shot, fallbackKey = "") {
  const requestGeneration = ++shotEffectRequestGeneration;
  const key = buildShotEffectKey(team, shot, fallbackKey);
  pendingShotEffect.value = { generation: requestGeneration, team, key };
  clearTipTimer();
  if (team === "red") latestOne.value = null;
  if (team === "blue") bluelatestOne.value = null;

  const viewportMode = props.stableShotEffect
    ? await updateTargetRect()
    : false;

  if (requestGeneration !== shotEffectRequestGeneration) {
    if (pendingShotEffect.value?.generation === requestGeneration) {
      pendingShotEffect.value = null;
    }
    return;
  }
  pendingShotEffect.value = null;
  triggerShotEffect(team, shot, fallbackKey, viewportMode);
}

function completeShotEffect(key) {
  if (!shotEffect.value || shotEffect.value.key !== key) return;

  const { team, shot } = shotEffect.value;
  if (team === "red") hiddenRedLatestKey.value = "";
  if (team === "blue") hiddenBlueLatestKey.value = "";
  shotEffect.value = null;
  showShotTip(team, shot);
}

function shakeTarget() {
  targetShaking.value = false;
  if (shakeTimer.value) {
    clearTimeout(shakeTimer.value);
    shakeTimer.value = null;
  }

  nextTick(() => {
    targetShaking.value = true;
    shakeTimer.value = setTimeout(() => {
      targetShaking.value = false;
      shakeTimer.value = null;
    }, 260);
  });
}

function hasValidTargetRect(rect = targetRect.value) {
  return (
    Number.isFinite(Number(rect?.left)) &&
    Number.isFinite(Number(rect?.top)) &&
    Number(rect?.width) > 0 &&
    Number(rect?.height) > 0
  );
}

async function updateTargetRect() {
  await nextTick();

  return new Promise((resolve) => {
    let settled = false;
    const finish = (rect) => {
      if (settled) return;
      settled = true;

      const isValid = hasValidTargetRect(rect);
      if (isValid) {
        targetRect.value = {
          left: Number(rect.left),
          top: Number(rect.top),
          width: Number(rect.width),
          height: Number(rect.height),
        };
      }
      resolve(isValid);
    };

    try {
      const query = instance?.proxy
        ? uni.createSelectorQuery().in(instance.proxy)
        : uni.createSelectorQuery();

      query
        .select(".target")
        .boundingClientRect()
        .exec((result) => finish(Array.isArray(result) ? result[0] : null));
    } catch {
      finish(null);
    }
  });
}

function handleWindowResize() {
  void updateTargetRect();
}

function shouldHideRedHit(index) {
  return (
    (!!hiddenRedLatestKey.value || pendingShotEffect.value?.team === "red") &&
    index === props.scores.length - 1
  );
}

function shouldHideBlueHit(index) {
  return (
    (!!hiddenBlueLatestKey.value || pendingShotEffect.value?.team === "blue") &&
    index === props.blueScores.length - 1
  );
}

function showShotFlash(flash) {
  const shootData = flash?.shootData;
  if (!shootData) {
    shotEffectRequestGeneration += 1;
    pendingShotEffect.value = null;
    hiddenRedLatestKey.value = "";
    hiddenBlueLatestKey.value = "";
    shotEffect.value = null;
    return;
  }

  const team = flash.team === "red" ? "red" : "blue";
  if (shouldPlayShotEffect(shootData, team)) {
    void prepareShotEffect(team, shootData, flash.key);
    return;
  }

  shotEffectRequestGeneration += 1;
  pendingShotEffect.value = null;
  showShotTip(team, shootData);
}

watch(
  () => props.latestShotFlash,
  (newVal) => {
    showShotFlash(newVal);
  },
  { immediate: true }
);

watch(
  () => props.scores.length,
  (newLen, oldLen) => {
    if (newLen > oldLen) return;
    shotEffectRequestGeneration += 1;
    pendingShotEffect.value = null;
    latestOne.value = null;
    hiddenRedLatestKey.value = "";
    if (shotEffect.value?.team === "red") shotEffect.value = null;
  }
);

watch(
  () => props.blueScores.length,
  (newLen, oldLen) => {
    if (newLen > oldLen) return;
    shotEffectRequestGeneration += 1;
    pendingShotEffect.value = null;
    bluelatestOne.value = null;
    hiddenBlueLatestKey.value = "";
    if (shotEffect.value?.team === "blue") shotEffect.value = null;
  }
);

const safeTargetRadius = computed(() => {
  const radius = Number(props.targetRadius);
  return Number.isFinite(radius) && radius > 0 ? radius : 20;
});

const currentHitRadiusPx = computed(() => {
  const radius = Number(
    pMode.value ? props.zoomHitRadiusPx : props.hitRadiusPx
  );
  return Number.isFinite(radius) && radius >= 0 ? radius : 0;
});

function getShotPoint(shot, fallbackCenter = false) {
  const x = Number(shot?.x);
  const y = Number(shot?.y);
  if (Number.isFinite(x) && Number.isFinite(y)) return { x, y };
  return fallbackCenter ? { x: 0, y: 0 } : null;
}

function getPointDirection(point) {
  if (!point) return null;
  const distance = Math.sqrt(point.x * point.x + point.y * point.y);
  if (distance === 0) return null;

  return {
    x: point.x / distance,
    y: point.y / distance,
  };
}

function formatPxOffset(value) {
  if (!value) return "";
  const operator = value > 0 ? "+" : "-";
  return ` ${operator} ${Math.abs(value)}px`;
}

function formatTargetPosition(percent, offset) {
  const pxOffset = formatPxOffset(offset);
  return pxOffset ? `calc(${percent}%${pxOffset})` : `${percent}%`;
}

function getTargetPositionStyle(point, offsetPx = 0, extraOffset = {}) {
  if (!point) return { display: "none" };

  const radius = safeTargetRadius.value;
  const diameter = radius * 2;
  const direction = getPointDirection(point);
  const xOffset = (direction ? direction.x * offsetPx : 0) + (extraOffset.x || 0);
  const yOffset = (direction ? -direction.y * offsetPx : 0) + (extraOffset.y || 0);
  const leftPercent = ((point.x + radius) / diameter) * 100;
  const topPercent = ((radius - point.y) / diameter) * 100;

  return {
    left: formatTargetPosition(leftPercent, xOffset),
    top: formatTargetPosition(topPercent, yOffset),
    transform: "translate(-50%, -50%)",
  };
}

function getHitStyle(shot) {
  const radius = currentHitRadiusPx.value;
  const point = getShotPoint(shot);

  return {
    ...getTargetPositionStyle(point, radius),
    width: `${radius * 2}px`,
    height: `${radius * 2}px`,
  };
}

function getSvipHitBgStyle(shot) {
  const radius = currentHitRadiusPx.value;
  const point = getShotPoint(shot);

  return {
    ...getTargetPositionStyle(point, radius),
  };
}

function getRoundTipStyle(shot) {
  const point = getShotPoint(shot, true);
  return getTargetPositionStyle(
    point,
    shot?.ring ? currentHitRadiusPx.value : 0,
    { y: ROUND_TIP_OFFSET_Y }
  );
}

function getExperienceTipStyle(shot) {
  const point = getShotPoint(shot, true);
  return getTargetPositionStyle(
    point,
    shot?.ring ? currentHitRadiusPx.value : 0,
    { y: EXPERIENCE_TIP_OFFSET_Y }
  );
}
const simulShoot = async () => {
  if (device.value.deviceId) await simulShootAPI(device.value.deviceId);
};
const simulShoot2 = async () => {
  if (device.value.deviceId) {
    const r1 = Math.random() > 0.5 ? 0.01 : 0.02;
    await simulShootAPI(device.value.deviceId, r1, r1);
  }
};

const openAim = async () => {
  await laserAimAPI();
};

const closeAim = async () => {
  await laserCloseAPI();
};

const env = computed(() => {
  const accountInfo = uni.getAccountInfoSync();
  return accountInfo.miniProgram.envVersion;
});

const arrowStyle = computed(() => {
  return {
    transform: `rotateX(180deg) translate(-50%, -50%) rotate(${
      360 - angle.value
    }deg) translateY(105%)`,
  };
});

async function onReceiveMessage(message) {
  if (Array.isArray(message)) return;
  if (message.type === MESSAGETYPESV2.ShootResult && message.shootData) {
    if (
      message.shootData.playerId === user.value.id &&
      !message.shootData.ring &&
      message.shootData.angle >= 0
    ) {
      angle.value = null;
      setTimeout(() => {
        if (props.scores[0]) {
          circleColor.value =
            message.shootData.playerId === props.scores[0].playerId
              ? "#ff4444"
              : "#1840FF";
        }
        angle.value = message.shootData.angle;
      }, 200);
    }
  }
}

onMounted(() => {
  uni.$on("socket-inbox", onReceiveMessage);
  void updateTargetRect();
  if (uni.onWindowResize) uni.onWindowResize(handleWindowResize);
});

onBeforeUnmount(() => {
  shotEffectRequestGeneration += 1;
  pendingShotEffect.value = null;
  if (timer.value) {
    clearTimeout(timer.value);
    timer.value = null;
  }
  if (dirTimer.value) {
    clearTimeout(dirTimer.value);
    dirTimer.value = null;
  }
  if (shakeTimer.value) {
    clearTimeout(shakeTimer.value);
    shakeTimer.value = null;
  }
  uni.$off("socket-inbox", onReceiveMessage);
  if (uni.offWindowResize) uni.offWindowResize(handleWindowResize);
});
</script>

<template>
  <view :class="['container', { 'container--effecting': shotEffect }]">
    <view class="header" v-if="totalRound > 0">
      <text v-if="totalRound > 0" class="round-count">{{
        (currentRound > totalRound ? totalRound : currentRound) +
        "/" +
        totalRound
      }}</text>
    </view>
    <view :class="['target', { 'target--shake': targetShaking }]">
      <view v-if="angle !== null" class="arrow-dir" :style="arrowStyle">
        <view :style="{ background: circleColor }">
          <image src="../../../static/dot-circle.png" mode="widthFix" />
        </view>
      </view>
      <view v-if="stop" class="stop-sign">中场休息</view>
      <view
        v-if="latestOne && latestOne.ring && user.id === latestOne.playerId"
        class="e-value fade-in-out"
        :style="getExperienceTipStyle(latestOne)"
      >
        经验 +1
      </view>
      <view
        v-if="latestOne"
        class="round-tip fade-in-out"
        :style="getRoundTipStyle(latestOne)"
        >{{ latestOne.ringX ? "X" : latestOne.ring || "未上靶"
        }}<text v-if="latestOne.ring">环</text>
      </view>
      <view
        v-if="
          bluelatestOne &&
          bluelatestOne.ring &&
          user.id === bluelatestOne.playerId
        "
        class="e-value fade-in-out"
        :style="getExperienceTipStyle(bluelatestOne)"
      >
        经验 +1
      </view>
      <view
        v-if="bluelatestOne"
        class="round-tip fade-in-out"
        :style="getRoundTipStyle(bluelatestOne)"
        >{{ bluelatestOne.ringX ? "X" : bluelatestOne.ring || "未上靶"
        }}<text v-if="bluelatestOne.ring">环</text></view
      >
      <block v-for="(bow, index) in scores" :key="index">
        <image
          v-if="
            pMode &&
            bow.ring > 0 &&
            isSvipShot(bow, 'red') &&
            !shouldHideRedHit(index)
          "
          class="svip-hit-bg"
          src="../../../static/vip/svip-xuan.png"
          :style="getSvipHitBgStyle(bow)"
          mode="aspectFit"
        />
        <view
          v-if="bow.ring > 0 && !shouldHideRedHit(index)"
          :class="`hit ${pMode ? 'b' : 's'}-point ${
            index === scores.length - 1 && latestOne ? 'pump-in' : ''
          }`"
          :style="{
            ...getHitStyle(bow),
            backgroundColor: mode === 'solo' ? '#00bf04' : '#FF0000',
          }"
          ><text v-if="pMode">{{ index + 1 }}</text></view
        >
      </block>
      <block v-for="(bow, index) in blueScores" :key="index">
        <image
          v-if="
            pMode &&
            bow.ring > 0 &&
            isSvipShot(bow, 'blue') &&
            !shouldHideBlueHit(index)
          "
          class="svip-hit-bg"
          src="../../../static/vip/svip-xuan.png"
          :style="getSvipHitBgStyle(bow)"
          mode="aspectFit"
        />
        <view
          v-if="bow.ring > 0 && !shouldHideBlueHit(index)"
          :class="`hit ${pMode ? 'b' : 's'}-point ${
            index === blueScores.length - 1 && bluelatestOne ? 'pump-in' : ''
          }`"
          :style="{
            ...getHitStyle(bow),
            backgroundColor: '#1840FF',
          }"
        >
          <text v-if="pMode">{{ index + 1 }}</text>
        </view>
      </block>
      <BowShotEffect
        v-if="!shotEffect || !shotEffect.viewportMode"
        :shot="shotEffect && shotEffect.shot"
        :playKey="shotEffect ? shotEffect.key : ''"
        :targetRadius="safeTargetRadius"
        :targetLeft="targetRect.left"
        :targetTop="targetRect.top"
        :targetWidth="targetRect.width"
        :targetHeight="targetRect.height"
        :hitOffsetPx="currentHitRadiusPx"
        @impact="shakeTarget"
        @complete="completeShotEffect"
      />
      <image src="https://static.shelingxingqiu.com/shootmini/static/bow-target.png" mode="widthFix" />
    </view>
    <BowShotEffect
      v-if="shotEffect && shotEffect.viewportMode"
      :shot="shotEffect.shot"
      :playKey="shotEffect.key"
      :targetRadius="safeTargetRadius"
      :targetLeft="targetRect.left"
      :targetTop="targetRect.top"
      :targetWidth="targetRect.width"
      :targetHeight="targetRect.height"
      :hitOffsetPx="currentHitRadiusPx"
      :viewportMode="true"
      @impact="shakeTarget"
      @complete="completeShotEffect"
    />
    <view class="footer">
      <PointSwitcher
        :onChange="(val) => (pMode = val)"
        :style="{ zIndex: 999 }"
      />
    </view>
    <view class="simul" v-if="env !== 'release'">
      <button @click="simulShoot">模拟</button>
      <button @click="simulShoot2">射箭</button>
      <button @click="openAim">开瞄</button>
      <button @click="closeAim">关瞄</button>
    </view>
  </view>
</template>

<style scoped>
.container {
  width: calc(100vw - 30px);
  height: calc(100vw - 30px);
  padding: 0px 15px;
  position: relative;
  z-index: 3;
}
.container--effecting {
  z-index: 10000;
}
.target {
  position: relative;
  margin: 10px;
  width: calc(100% - 20px);
  height: calc(100% - 20px);
  z-index: 1;
  pointer-events: none;
  transform-origin: center center;
}
.target--shake {
  animation: target-shake 0.26s ease-out;
}
.e-value {
  position: absolute;
  background-color: #0006;
  color: #fff;
  font-size: 12px;
  padding: 4px 7px;
  border-radius: 5px;
  z-index: 2;
  width: 50px;
  text-align: center;
}
.round-tip {
  position: absolute;
  color: #fff;
  font-size: 30px;
  font-weight: bold;
  z-index: 2;
  width: 100px;
  text-align: center;
}
.round-tip > text {
  font-size: 24px;
  margin-left: 5px;
}
@keyframes target-tip-fade-in-out {
  0% {
    transform: translate(-50%, -50%) translateY(20px);
    opacity: 0;
  }

  30% {
    transform: translate(-50%, -50%);
    opacity: 1;
  }

  80% {
    transform: translate(-50%, -50%);
    opacity: 1;
  }

  100% {
    transform: translate(-50%, -50%);
    opacity: 0;
  }
}
.round-tip.fade-in-out,
.e-value.fade-in-out {
  animation: target-tip-fade-in-out 1.2s ease forwards;
}
.target > image:last-child {
  width: 100%;
  height: 100%;
}
.svip-hit-bg {
  position: absolute;
  width: 48rpx;
  height: 48rpx;
  z-index: 1;
  pointer-events: none;
  transform-origin: center center;
  animation: svip-hit-xuan 1.2s linear infinite;
}
.hit {
  position: absolute;
  border-radius: 50%;
  z-index: 2;
  color: #fff;
  transition: transform 0.2s ease, opacity 0.2s ease;
  box-sizing: border-box;
}
.b-point {
  border: 1px solid #fff;
  z-index: 2;
  display: flex;
  justify-content: center;
  align-items: center;
}
.b-point > text {
  font-size: 16rpx;
  color: #fff;
  font-family: "DINCondensed";
  /* text-align: center;
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);*/
  margin-top: 2rpx;
}
@keyframes svip-hit-xuan {
  0% {
    opacity: 0.9;
    transform: translate(-50%, -50%) rotate(0deg) scale(0.92);
  }
  50% {
    opacity: 1;
    transform: translate(-50%, -50%) rotate(180deg) scale(1.08);
  }
  100% {
    opacity: 0.9;
    transform: translate(-50%, -50%) rotate(360deg) scale(0.92);
  }
}
@keyframes target-pump-in {
  from {
    transform: translate(-50%, -50%) scale(2);
  }

  to {
    transform: translate(-50%, -50%) scale(1);
  }
}
@keyframes target-shake {
  0% {
    transform: translate(0, 0);
  }
  14% {
    transform: translate(-20rpx, 8rpx);
  }
  28% {
    transform: translate(16rpx, -8rpx);
  }
  44% {
    transform: translate(-12rpx, 6rpx);
  }
  64% {
    transform: translate(8rpx, -4rpx);
  }
  82% {
    transform: translate(-4rpx, 2rpx);
  }
  100% {
    transform: translate(0, 0);
  }
}
.hit.pump-in {
  animation: target-pump-in 0.3s ease-out forwards;
  transform-origin: center center;
}
.header {
  width: 100%;
  display: flex;
  align-items: center;
  justify-content: space-between;
  margin-bottom: -40px;
}
.header > image:first-child {
  width: 40px;
  height: 40px;
}
.round-count {
  font-size: 20px;
  color: #fed847;
  top: 75px;
  font-weight: bold;
}
.footer {
  width: calc(100% - 20px);
  padding: 0 10px;
  display: flex;
  margin-top: -40px;
  justify-content: flex-end;
  position: relative;
  z-index: 999;
}
.footer > image {
  width: 40px;
  min-height: 40px;
  max-height: 40px;
  border-radius: 50%;
  border: 1px solid #fff;
}
.simul {
  position: absolute;
  top: 0;
  right: 20px;
  margin-left: 20px;
  z-index: 999;
}
.simul > button {
  color: #fff;
}
.stop-sign {
  position: absolute;
  font-size: 44px;
  color: #fff9;
  text-align: center;
  width: 200px;
  height: 60px;
  left: calc(50% - 100px);
  top: calc(50% - 30px);
  z-index: 99;
  font-weight: bold;
}
.arrow-dir {
  position: absolute;
  width: 100%;
  height: 52%;
  left: 50%;
  bottom: 50%;
  display: flex;
  align-items: center;
  justify-content: center;
}
.arrow-dir > view {
  width: 40rpx;
  height: 40rpx;
  border-radius: 50%;
}
.arrow-dir > view > image {
  width: 100rpx;
  height: 100rpx;
  transform: translate(-30%, -30%);
}
@keyframes spring-in {
  0% {
    transform: scale(2);
    opacity: 0.4;
  }
  15% {
    transform: scale(3);
    opacity: 1;
  }
  30% {
    transform: scale(2);
    opacity: 0.4;
  }
  45% {
    transform: scale(3);
    opacity: 1;
  }
  60% {
    transform: scale(2);
    opacity: 0.4;
  }
  75% {
    transform: scale(3);
    opacity: 1;
  }
  100% {
    transform: scale(1);
    opacity: 0;
  }
}
@keyframes disappear {
  0% {
    opacity: 1;
  }
  75% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}
.arrow-dir > view {
  animation: disappear 3s ease forwards;
}
.arrow-dir > view > image {
  animation: spring-in 3s ease forwards;
  width: 100%;
}
</style>
