<template>
  <view class="template-carousel" :style="carouselStyle">
    <view class="template-heading">
      <view>
        <text class="template-heading-title">选择模板</text>
        <text class="template-heading-subtitle">左右滑动预览效果</text>
      </view>
    </view>

    <scroll-view class="template-rail" scroll-x show-scrollbar="false" scroll-with-animation
      :scroll-left="railScrollLeft" @scroll="handleScroll" @touchstart="handleTouchStart" @touchend="handleTouchEnd"
      @touchcancel="handleTouchEnd">
      <view class="template-track">
        <TemplateCard v-for="(template, index) in templates" :key="template.templateId || index" :template="template"
          :active="index === modelValue" @select="handleSelect(template, index)" />
      </view>
    </scroll-view>

    <view class="template-dots">
      <view v-for="(template, index) in templates" :key="`${template.templateId || index}-dot`" class="template-dot"
        :class="{ 'is-active': index === modelValue }" />
    </view>
  </view>
</template>

<script setup>
import { computed, getCurrentInstance, nextTick, onMounted, onUnmounted, ref, watch } from "vue";
import TemplateCard from "../TemplateCard/index.vue";

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

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

const BASE_CARD_WIDTH = 318;
const BASE_CARD_HEIGHT = 494;
const BASE_MEDIA_HEIGHT = 358;
const BASE_RAIL_EXTRA_HEIGHT = 18;
const BASE_RAIL_HEIGHT = BASE_CARD_HEIGHT + BASE_RAIL_EXTRA_HEIGHT;
const BASE_TRACK_GAP = 12;
const BASE_TRACK_END_PADDING = 41;
const BASE_SIDE_PADDING = 16;
const BASE_SWITCH_THRESHOLD = 88;
const HEADING_TOTAL_HEIGHT = 54;
const DOTS_TOTAL_HEIGHT = 19;
const FLOW_STEPS_TOTAL_HEIGHT = 67;
const PAGE_BOTTOM_RESERVE = 8;
const MIN_CARD_SCALE = 0.72;
const MAX_CARD_SCALE = 1;

const screenWidth = ref(375);
const screenHeight = ref(812);
const cardScale = ref(1);
const railScrollLeft = ref(0);
const currentScrollLeft = ref(0);
const instance = getCurrentInstance();

let touchStartLeft = 0;
let touchStartIndex = 0;
let isTouching = false;
let settleTimer = null;

const getMaxIndex = () => Math.max(props.templates.length - 1, 0);

const clampIndex = (index) => Math.max(0, Math.min(getMaxIndex(), index));

const cardWidth = computed(() => Math.round(BASE_CARD_WIDTH * cardScale.value));
const cardHeight = computed(() => Math.round(BASE_CARD_HEIGHT * cardScale.value));
const mediaHeight = computed(() => Math.round(BASE_MEDIA_HEIGHT * cardScale.value));
const copyHeight = computed(() => Math.max(cardHeight.value - mediaHeight.value, 96));
const railHeight = computed(() => Math.round(BASE_RAIL_HEIGHT * cardScale.value));
const trackGap = computed(() => Math.round(BASE_TRACK_GAP * cardScale.value));
const trackEndPadding = computed(() => Math.round(BASE_TRACK_END_PADDING * cardScale.value));
const cardStep = computed(() => cardWidth.value + trackGap.value);
const switchThreshold = computed(() => Math.round(BASE_SWITCH_THRESHOLD * cardScale.value));

const carouselStyle = computed(() => ({
  "--template-card-width": `${cardWidth.value}px`,
  "--template-card-height": `${cardHeight.value}px`,
  "--template-card-media-height": `${mediaHeight.value}px`,
  "--template-card-copy-height": `${copyHeight.value}px`,
  "--template-card-radius": `${Math.round(28 * cardScale.value)}px`,
  "--template-rail-height": `${railHeight.value}px`,
  "--template-track-gap": `${trackGap.value}px`,
  "--template-track-end-padding": `${trackEndPadding.value}px`,
  "--template-side-padding": `${BASE_SIDE_PADDING}px`,
}));

const getScreenSize = () => {
  try {
    const systemInfo = uni.getSystemInfoSync();
    return {
      width: Number(systemInfo.windowWidth) || 375,
      height: Number(systemInfo.windowHeight) || 812,
    };
  } catch (error) {
    return {
      width: 375,
      height: 812,
    };
  }
};

const getCarouselTop = () =>
  new Promise((resolve) => {
    try {
      let query = uni.createSelectorQuery();
      if (instance?.proxy && typeof query.in === "function") {
        query = query.in(instance.proxy);
      }

      query
        .select(".template-carousel")
        .boundingClientRect((rect) => {
          resolve(Number(rect?.top) || 0);
        })
        .exec();
    } catch (error) {
      resolve(0);
    }
  });

const getNextScale = (carouselTop = 0) => {
  const availableRailHeight =
    screenHeight.value -
    carouselTop -
    HEADING_TOTAL_HEIGHT -
    DOTS_TOTAL_HEIGHT -
    FLOW_STEPS_TOTAL_HEIGHT -
    PAGE_BOTTOM_RESERVE;
  const heightScale = availableRailHeight / BASE_RAIL_HEIGHT;
  const widthScale = (screenWidth.value - BASE_SIDE_PADDING * 2) / BASE_CARD_WIDTH;
  const nextScale = Math.min(heightScale, widthScale, MAX_CARD_SCALE);

  return Math.max(MIN_CARD_SCALE, nextScale);
};

const getCenterOffset = () => (screenWidth.value - cardWidth.value) / 2 - BASE_SIDE_PADDING;

const getTargetScrollLeft = (index) => {
  const targetIndex = clampIndex(index);
  if (targetIndex === 0) return 0;
  return Math.round(targetIndex * cardStep.value - getCenterOffset());
};

const getNearestIndex = (left) =>
  clampIndex(Math.round((left + getCenterOffset()) / cardStep.value));

const updateLayoutMetrics = async (size = {}) => {
  const screenSize = getScreenSize();
  screenWidth.value = Number(size.windowWidth) || screenSize.width;
  screenHeight.value = Number(size.windowHeight) || screenSize.height;

  await nextTick();

  const carouselTop = await getCarouselTop();
  cardScale.value = getNextScale(carouselTop);
  scrollToIndex(props.modelValue);
};

const handleWindowResize = (event) => {
  updateLayoutMetrics(event?.size);
};

const setRailScrollLeft = async (targetLeft, force = false) => {
  await nextTick();

  if (force && railScrollLeft.value === targetLeft) {
    railScrollLeft.value = targetLeft > 0 ? targetLeft - 1 : 1;
    setTimeout(() => {
      railScrollLeft.value = targetLeft;
      currentScrollLeft.value = targetLeft;
    }, 0);
    return;
  }

  railScrollLeft.value = targetLeft;
  currentScrollLeft.value = targetLeft;
};

const scrollToIndex = (index, force = false) => {
  setRailScrollLeft(getTargetScrollLeft(index), force);
};

const settleToIndex = (index, force = true) => {
  const targetIndex = clampIndex(index);
  if (targetIndex !== props.modelValue) {
    emit("update:modelValue", targetIndex);
  }
  scrollToIndex(targetIndex, force);
};

const handleScroll = (event) => {
  currentScrollLeft.value = event.detail?.scrollLeft || 0;

  if (!isTouching) {
    clearTimeout(settleTimer);
    settleTimer = setTimeout(() => {
      settleToIndex(getNearestIndex(currentScrollLeft.value));
    }, 140);
  }
};

const handleTouchStart = () => {
  clearTimeout(settleTimer);
  isTouching = true;
  touchStartLeft = currentScrollLeft.value;
  touchStartIndex = clampIndex(props.modelValue);
};

const handleTouchEnd = () => {
  if (!isTouching) return;

  isTouching = false;
  clearTimeout(settleTimer);

  const offset = currentScrollLeft.value - touchStartLeft;
  const direction = offset > 0 ? 1 : -1;
  const targetIndex =
    Math.abs(offset) >= switchThreshold.value
      ? touchStartIndex + direction
      : touchStartIndex;

  settleToIndex(targetIndex);
};

const handleSelect = (template, index) => {
  settleToIndex(index);
  emit("select", template, index);
};

watch(
  () => [props.modelValue, props.templates.length],
  () => {
    scrollToIndex(props.modelValue);
  },
  { immediate: true }
);

onMounted(() => {
  updateLayoutMetrics();
  if (typeof uni.onWindowResize === "function") {
    uni.onWindowResize(handleWindowResize);
  }
});

onUnmounted(() => {
  clearTimeout(settleTimer);
  if (typeof uni.offWindowResize === "function") {
    uni.offWindowResize(handleWindowResize);
  }
});
</script>

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