<script setup>
const props = defineProps({
  value: {
    type: String,
    default: '',
  },
  delayOffset: {
    type: Number,
    default: 0,
  },
  format: {
    type: Function,
    default: null,
  },
})

const displayValue = ref(extractNumber(props.value))
const previousText = ref(formatDisplay(displayValue.value))
const isAnimating = ref(false)
const direction = ref(1)

let updateTimer = null
let cleanupTimer = null

const currentText = computed(() => formatDisplay(displayValue.value))
const currentParts = computed(() => buildTextParts(currentText.value))
const previousParts = computed(() => buildTextParts(previousText.value))

function extractNumber(value) {
  const parsed = Number.parseInt(String(value || '').replace(/\D/g, ''), 10)

  return Number.isFinite(parsed) ? parsed : 0
}

function formatDisplay(number) {
  if (props.format) {
    return props.format(number)
  }

  const rawValue = String(props.value || '')

  return rawValue.replace(/\d[\d.,]*/, String(number))
}

function buildTextParts(text) {
  let digitIndex = 0

  return String(text || '').split('').map((char, index) => {
    const delay = `${digitIndex * 60}ms`

    if (/\d/.test(char)) {
      digitIndex += 1
    }

    return {
      id: `${index}-${char}`,
      char,
      delay,
    }
  })
}

function clearAnimationTimers() {
  if (updateTimer) {
    clearTimeout(updateTimer)
    updateTimer = null
  }

  if (cleanupTimer) {
    clearTimeout(cleanupTimer)
    cleanupTimer = null
  }
}

watch(() => props.value, (nextValue) => {
  const nextNumber = extractNumber(nextValue)

  if (nextNumber === displayValue.value) {
    return
  }

  clearAnimationTimers()
  direction.value = nextNumber > displayValue.value ? 1 : -1

  updateTimer = setTimeout(() => {
    previousText.value = currentText.value
    displayValue.value = nextNumber
    isAnimating.value = true

    cleanupTimer = setTimeout(() => {
      isAnimating.value = false
      previousText.value = currentText.value
    }, 700)
  }, 500 + props.delayOffset)
})

onBeforeUnmount(() => {
  clearAnimationTimers()
})
</script>

<template>
  <span
    :class="[
      'animated-offer-number',
      isAnimating && 'is-animating',
      direction < 0 && 'is-descending',
    ]"
  >
    <span
      v-if="isAnimating"
      class="animated-offer-number__layer animated-offer-number__layer--previous"
      aria-hidden="true"
    >
      <span
        v-for="part in previousParts"
        :key="`previous-${part.id}`"
        class="animated-offer-number__char"
        :style="{ '--digit-delay': part.delay }"
      >
        {{ part.char }}
      </span>
    </span>
    <span class="animated-offer-number__layer animated-offer-number__layer--current">
      <span
        v-for="part in currentParts"
        :key="`current-${currentText}-${part.id}`"
        class="animated-offer-number__char"
        :style="{ '--digit-delay': part.delay }"
      >
        {{ part.char }}
      </span>
    </span>
  </span>
</template>

<style scoped>
.animated-offer-number {
  display: inline-grid;
  overflow: hidden;
  perspective: 200px;
  vertical-align: bottom;
}

.animated-offer-number__layer {
  display: inline-flex;
  grid-area: 1 / 1;
  align-items: baseline;
}

.animated-offer-number__char {
  display: inline-block;
  transform-origin: center;
  white-space: pre;
}

.animated-offer-number.is-animating .animated-offer-number__layer--current .animated-offer-number__char {
  animation: offer-number-enter 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) both;
  animation-delay: var(--digit-delay, 0ms);
}

.animated-offer-number.is-animating .animated-offer-number__layer--previous .animated-offer-number__char {
  animation: offer-number-exit 0.5s cubic-bezier(0.2, 0.8, 0.2, 1) both;
  animation-delay: var(--digit-delay, 0ms);
}

.animated-offer-number.is-descending .animated-offer-number__layer--current .animated-offer-number__char {
  animation-name: offer-number-enter-down;
}

.animated-offer-number.is-descending .animated-offer-number__layer--previous .animated-offer-number__char {
  animation-name: offer-number-exit-down;
}

@keyframes offer-number-enter {
  0% {
    opacity: 0;
    filter: blur(4px);
    transform: translateY(25px) rotateX(-60deg);
  }

  100% {
    opacity: 1;
    filter: blur(0);
    transform: translateY(0) rotateX(0);
  }
}

@keyframes offer-number-exit {
  0% {
    opacity: 1;
    filter: blur(0);
    transform: translateY(0) rotateX(0);
  }

  100% {
    opacity: 0;
    filter: blur(4px);
    transform: translateY(-25px) rotateX(60deg);
  }
}

@keyframes offer-number-enter-down {
  0% {
    opacity: 0;
    filter: blur(4px);
    transform: translateY(-25px) rotateX(60deg);
  }

  100% {
    opacity: 1;
    filter: blur(0);
    transform: translateY(0) rotateX(0);
  }
}

@keyframes offer-number-exit-down {
  0% {
    opacity: 1;
    filter: blur(0);
    transform: translateY(0) rotateX(0);
  }

  100% {
    opacity: 0;
    filter: blur(4px);
    transform: translateY(25px) rotateX(-60deg);
  }
}
</style>
