<template>
  <view
    class="card-swiper"
    :class="{ 'is-single': props.list.length <= 1 }"
    @touchstart="handleTouchStart"
    @touchmove.stop.prevent="handleTouchMove"
    @touchend="handleTouchEnd"
    @touchcancel="handleTouchCancel"
  >
    <view class="swiper-stage">
      <view
        v-for="slot in renderSlots"
        :key="slot.key"
        class="swiper-card"
        :class="[`is-${slot.role}`, { 'is-current': slot.role === 'current' }]"
        :style="slot.style"
        @tap="handleCardTap(slot)"
      >
        <view class="card-shell">
          <view class="card-media">
            <image class="card-image" :src="slot.item.coverImage" mode="aspectFill" />
          </view>

          <view class="card-body">
            <view v-if="slot.item.tag" class="card-tag">
              {{ slot.item.tag }}
            </view>
            <view class="card-title ellipsis-1">
              {{ slot.item.title }}
            </view>
            <view v-if="slot.item.subTitle" class="card-desc ellipsis-1">
              {{ slot.item.subTitle }}
            </view>
          </view>

          <view
            v-if="canSwipe"
            class="card-mask"
            :style="getMaskStyle(slot.role)"
          />
        </view>
      </view>
    </view>
  </view>
</template>

<script setup>
import { computed, getCurrentInstance, onBeforeUnmount, ref, watch } from "vue";

const props = defineProps({
  modelValue: {
    type: Number,
    default: 0,
  },
  list: {
    type: Array,
    default: () => [],
  },
});

const emit = defineEmits(["update:modelValue", "change", "didSelectItem"]);

const DURATION = 280;
const CLICK_THRESHOLD = 8;
const SWIPE_THRESHOLD = 60;

const clamp = (value, min, max) => Math.min(Math.max(value, min), max);
const lerp = (from, to, progress) => from + (to - from) * progress;

const { windowWidth = 375 } = uni.getWindowInfo();
const sideOffset = Math.max(108, Math.min(windowWidth * 0.26, 148));
const hiddenOffset = sideOffset + 92;

const activeCursor = ref(0);
const deltaX = ref(0);
const isDragging = ref(false);
const isAnimating = ref(false);
const isTapCandidate = ref(false);
const swipeStep = ref(0);

let startX = 0;
let startY = 0;
let settleTimer = null;
const instance = getCurrentInstance();
const hasExternalModel = Object.prototype.hasOwnProperty.call(
  instance?.vnode.props || {},
  "modelValue"
);

const actualCount = computed(() => props.list.length);
const virtualCount = computed(() => {
  if (actualCount.value === 1) return 3;
  return actualCount.value;
});
const canSwipe = computed(() => virtualCount.value > 1);
const progress = computed(() => {
  if (!canSwipe.value) return 0;
  return clamp(deltaX.value / sideOffset, -1, 1);
});

const normalizeIndex = (index, total) => {
  if (!total) return 0;
  return ((index % total) + total) % total;
};

const getActualIndex = (virtualIndex) => {
  if (!actualCount.value) return 0;
  return normalizeIndex(virtualIndex, actualCount.value);
};

const getItemByVirtualIndex = (virtualIndex) => {
  if (!actualCount.value) return null;
  return props.list[getActualIndex(virtualIndex)] || null;
};

const syncActiveCursor = (incomingIndex = 0) => {
  activeCursor.value = normalizeIndex(incomingIndex, virtualCount.value);
};

function clearSettleTimer() {
  if (settleTimer) {
    clearTimeout(settleTimer);
    settleTimer = null;
  }
}

watch(
  () => props.list,
  () => {
    clearSettleTimer();
    deltaX.value = 0;
    isDragging.value = false;
    isAnimating.value = false;
    const nextCursor = hasExternalModel
      ? props.modelValue
      : activeCursor.value;
    syncActiveCursor(nextCursor);
  },
  { deep: true, immediate: true }
);

watch(
  () => props.modelValue,
  (value) => {
    if (!hasExternalModel) return;
    if (isAnimating.value || isDragging.value) return;
    syncActiveCursor(value);
  }
);

const getItemKey = (virtualIndex, role) => {
  const item = getItemByVirtualIndex(virtualIndex) || {};
  const baseKey =
    item.id ??
    item.commodityId ??
    item.tabLabel ??
    item.title ??
    virtualIndex;

  return `${baseKey}-${virtualIndex}-${role}`;
};

const states = {
  hiddenLeft: {
    x: -hiddenOffset,
    scale: 0.68,
    opacity: 0,
  },
  left: {
    x: -sideOffset,
    scale: 1 / 1.2,
    opacity: 0.36,
  },
  center: {
    x: 0,
    scale: 1,
    opacity: 1,
  },
  right: {
    x: sideOffset,
    scale: 1 / 1.2,
    opacity: 0.36,
  },
  hiddenRight: {
    x: hiddenOffset,
    scale: 0.68,
    opacity: 0,
  },
};

const interpolateState = (fromKey, toKey, rate) => {
  const from = states[fromKey];
  const to = states[toKey];
  return {
    x: lerp(from.x, to.x, rate),
    scale: lerp(from.scale, to.scale, rate),
    opacity: lerp(from.opacity, to.opacity, rate),
  };
};

const getLayerDirection = () => {
  if (isDragging.value && deltaX.value !== 0) {
    return deltaX.value < 0 ? 1 : -1;
  }
  if (isAnimating.value) {
    return swipeStep.value;
  }
  return 0;
};

const getCardZIndex = (role) => {
  const direction = getLayerDirection();

  if (direction === 1) {
    if (role === "next") return 4;
    if (role === "current") return 3;
    return 1;
  }

  if (direction === -1) {
    if (role === "prev") return 4;
    if (role === "current") return 3;
    return 1;
  }

  if (role === "current") return 3;
  return 1;
};

const getSlotState = (role) => {
  if (!canSwipe.value) return states.center;

  const currentProgress = progress.value;
  if (currentProgress < 0) {
    const rate = Math.abs(currentProgress);
    if (role === "prev") return interpolateState("left", "hiddenLeft", rate);
    if (role === "current") return interpolateState("center", "left", rate);
    return interpolateState("right", "center", rate);
  }

  if (currentProgress > 0) {
    const rate = currentProgress;
    if (role === "prev") return interpolateState("left", "center", rate);
    if (role === "current") return interpolateState("center", "right", rate);
    return interpolateState("right", "hiddenRight", rate);
  }

  if (role === "prev") return states.left;
  if (role === "next") return states.right;
  return states.center;
};

const buildCardStyle = (role) => {
  const state = getSlotState(role);
  return {
    transform: `translate3d(-50%, 0, 0) translateX(${state.x}px) scale(${state.scale})`,
    opacity: state.opacity,
    zIndex: getCardZIndex(role),
    transition: isDragging.value
      ? "none"
      : `transform ${DURATION}ms ease, opacity ${DURATION}ms ease`,
  };
};

const getMaskOpacity = (role) => {
  if (!canSwipe.value) return 0;

  const currentProgress = progress.value;
  const baseOpacity = 0.42;

  if (currentProgress < 0) {
    const rate = Math.abs(currentProgress);
    if (role === "next") return baseOpacity * (1 - rate);
    if (role === "current") return baseOpacity * rate;
    return baseOpacity;
  }

  if (currentProgress > 0) {
    const rate = currentProgress;
    if (role === "prev") return baseOpacity * (1 - rate);
    if (role === "current") return baseOpacity * rate;
    return baseOpacity;
  }

  return role === "current" ? 0 : baseOpacity;
};

const getMaskStyle = (role) => ({
  opacity: getMaskOpacity(role),
  transition: isDragging.value ? "none" : "opacity 120ms ease-out",
});

const renderSlots = computed(() => {
  if (!actualCount.value) return [];

  const prevIndex = normalizeIndex(activeCursor.value - 1, virtualCount.value);
  const nextIndex = normalizeIndex(activeCursor.value + 1, virtualCount.value);

  return [
    {
      key: getItemKey(prevIndex, "prev"),
      role: "prev",
      index: getActualIndex(prevIndex),
      item: getItemByVirtualIndex(prevIndex),
      style: buildCardStyle("prev"),
    },
    {
      key: getItemKey(activeCursor.value, "current"),
      role: "current",
      index: getActualIndex(activeCursor.value),
      item: getItemByVirtualIndex(activeCursor.value),
      style: buildCardStyle("current"),
    },
    {
      key: getItemKey(nextIndex, "next"),
      role: "next",
      index: getActualIndex(nextIndex),
      item: getItemByVirtualIndex(nextIndex),
      style: buildCardStyle("next"),
    },
  ];
});

const resetGesture = () => {
  deltaX.value = 0;
  isDragging.value = false;
  isAnimating.value = false;
  isTapCandidate.value = false;
  swipeStep.value = 0;
};

const finishSwipe = (step) => {
  isAnimating.value = true;
  swipeStep.value = step;
  deltaX.value = step > 0 ? -sideOffset : sideOffset;
  clearSettleTimer();
  settleTimer = setTimeout(() => {
    const nextCursor = normalizeIndex(activeCursor.value + step, virtualCount.value);
    activeCursor.value = nextCursor;
    const actualIndex = getActualIndex(nextCursor);
    emit("update:modelValue", actualIndex);
    emit("change", actualIndex);
    resetGesture();
  }, DURATION);
};

const handleTouchStart = (event) => {
  if (!canSwipe.value || isAnimating.value) return;

  const touch = event.touches?.[0] || event.changedTouches?.[0];
  if (!touch) return;

  startX = touch.clientX;
  startY = touch.clientY;
  deltaX.value = 0;
  isDragging.value = true;
  isTapCandidate.value = true;
};

const handleTouchMove = (event) => {
  if (!canSwipe.value || !isDragging.value || isAnimating.value) return;

  const touch = event.touches?.[0] || event.changedTouches?.[0];
  if (!touch) return;

  const moveX = touch.clientX - startX;
  const moveY = touch.clientY - startY;

  if (Math.abs(moveX) > CLICK_THRESHOLD || Math.abs(moveY) > CLICK_THRESHOLD) {
    isTapCandidate.value = false;
  }

  if (Math.abs(moveY) > Math.abs(moveX) && Math.abs(moveY) > 12) {
    deltaX.value = 0;
    return;
  }

  deltaX.value = clamp(moveX, -hiddenOffset, hiddenOffset);
};

const handleTouchEnd = () => {
  if (!canSwipe.value || !isDragging.value) return;

  isDragging.value = false;

  if (isTapCandidate.value) {
    deltaX.value = 0;
    return;
  }

  if (Math.abs(deltaX.value) >= SWIPE_THRESHOLD) {
    finishSwipe(deltaX.value > 0 ? -1 : 1);
    return;
  }

  deltaX.value = 0;
};

const handleTouchCancel = () => {
  if (!canSwipe.value) return;
  clearSettleTimer();
  deltaX.value = 0;
  isDragging.value = false;
  isAnimating.value = false;
  isTapCandidate.value = false;
  swipeStep.value = 0;
};

const handleCardTap = (slot) => {
  if (slot.role !== "current" || isDragging.value || isAnimating.value) return;
  emit("didSelectItem", slot.item, slot.index);
};

onBeforeUnmount(() => {
  clearSettleTimer();
});
</script>

<style scoped lang="scss">
@import "./styles/index.scss";
</style>
