<template>
  <div class="w-full" @touchstart="handleTouchStart" @touchmove="handleTouchMove" @touchend="handleTouchEnd"
    @touchcancel="handleTouchCancel">
    <div :class="['relative w-full h-[270px] overflow-hidden', list.length <= 1 ? 'overflow-visible' : '']">
      <div v-for="slot in renderSlots" :key="slot.key"
        class="absolute left-1/2 top-2 w-[236px] h-[234px] max-w-[calc(100%-56px)] origin-center [will-change:transform,opacity]"
        :style="slot.style" @click="handleCardTap(slot)">
        <div
          :class="['relative w-full h-full p-2 overflow-hidden rounded-3xl bg-white', slot.role === 'current' ? '[box-shadow:0_12px_20px_rgba(15,23,42,0.18)]' : '[box-shadow:0_12px_20px_rgba(15,23,42,0.14)]']">
          <div class="w-full h-[142px] m-0 overflow-hidden rounded-[20px]">
            <img class="block w-full h-full" :src="slot.item.coverImage" />
          </div>

          <div class="px-2">
            <div v-if="slot.item.tag"
              class="inline-flex items-center justify-center mt-3 min-w-[50px] max-w-full h-[18px] px-2 rounded bg-[#fff4db] text-[#d78621] text-[9px] font-semibold">
              {{ slot.item.tag }}
            </div>
            <div class="mt-[6px] text-[#172033] text-[16px] leading-[1.2] font-bold truncate">
              {{ slot.item.title }}
            </div>
            <div v-if="slot.item.subTitle" class="mt-[2px] text-[#7f8ea3] text-[12px] leading-[18px] truncate">
              {{ slot.item.subTitle }}
            </div>
          </div>

          <div v-if="canSwipe" class="absolute inset-0 bg-white/42 pointer-events-none"
            :style="getMaskStyle(slot.role)"></div>
        </div>
      </div>
    </div>
  </div>
</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 RECYCLE_FRAME_DELAY = 32;
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 sideOffset = Math.max(108, Math.min(375 * 0.26, 148));
const hiddenOffset = sideOffset + 92;

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

let startX = 0;
let startY = 0;
let settleTimer = null;
let recycleTimer = 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 actualCount.value;
  if (actualCount.value === 2) return 4;
  return actualCount.value;
});
const canSwipe = computed(() => actualCount.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;
};

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

function clearRecycleTimer() {
  if (recycleTimer) {
    clearTimeout(recycleTimer);
    recycleTimer = null;
  }
}

const getCircularDistance = (from, to, total) => {
  const forward = normalizeIndex(to - from, total);
  const backward = normalizeIndex(from - to, total);
  return Math.min(forward, backward);
};

const getNearestVirtualIndexByActualIndex = (actualIndex, anchor = activeCursor.value) => {
  if (!actualCount.value || !virtualCount.value) return 0;

  const targetActualIndex = normalizeIndex(actualIndex, actualCount.value);
  let matchedIndex = targetActualIndex;
  let minDistance = Infinity;

  for (let index = 0; index < virtualCount.value; index += 1) {
    if (getActualIndex(index) !== targetActualIndex) continue;

    const distance = getCircularDistance(anchor, index, virtualCount.value);
    if (distance < minDistance) {
      matchedIndex = index;
      minDistance = distance;
    }
  }

  return matchedIndex;
};

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

const syncActiveCursorByActualIndex = (incomingIndex = 0) => {
  activeCursor.value = getNearestVirtualIndexByActualIndex(incomingIndex);
};

watch(
  () => props.list,
  () => {
    clearSettleTimer();
    clearRecycleTimer();
    deltaX.value = 0;
    isDragging.value = false;
    isAnimating.value = false;
    isRecycling.value = false;
    if (hasExternalModel) {
      syncActiveCursorByActualIndex(props.modelValue);
    } else {
      syncActiveCursorByVirtualIndex(activeCursor.value);
    }
  },
  { deep: true, immediate: true }
);

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

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

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

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 || isRecycling.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 || isRecycling.value ? "none" : "opacity 120ms ease-out",
});

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

  if (!canSwipe.value) {
    return [
      {
        key: getItemKey(activeCursor.value),
        role: "current",
        index: getActualIndex(activeCursor.value),
        item: getItemByVirtualIndex(activeCursor.value),
        style: buildCardStyle("current"),
      },
    ];
  }

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

  return [
    {
      key: getItemKey(prevIndex),
      role: "prev",
      index: getActualIndex(prevIndex),
      item: getItemByVirtualIndex(prevIndex),
      style: buildCardStyle("prev"),
    },
    {
      key: getItemKey(activeCursor.value),
      role: "current",
      index: getActualIndex(activeCursor.value),
      item: getItemByVirtualIndex(activeCursor.value),
      style: buildCardStyle("current"),
    },
    {
      key: getItemKey(nextIndex),
      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);
    isRecycling.value = true;
    activeCursor.value = nextCursor;
    const actualIndex = getActualIndex(nextCursor);
    emit("update:modelValue", actualIndex);
    emit("change", actualIndex);
    resetGesture();
    clearRecycleTimer();
    recycleTimer = setTimeout(() => {
      isRecycling.value = false;
      recycleTimer = null;
    }, RECYCLE_FRAME_DELAY);
  }, DURATION);
};

const handleTouchStart = (event) => {
  if (!canSwipe.value || isAnimating.value || isRecycling.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;
  }

  event.stopPropagation();
  if (event.cancelable) {
    event.preventDefault();
  }
  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();
  clearRecycleTimer();
  deltaX.value = 0;
  isDragging.value = false;
  isAnimating.value = false;
  isRecycling.value = false;
  isTapCandidate.value = false;
  swipeStep.value = 0;
};

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

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