<template>
  <div ref="cpTabsElement" class="cpTabs" :class="dynamicTabsClass" role="tablist">
    <button
      v-for="(tab, index) in tabs"
      :key="tab.title"
      class="cpTabs__tab"
      :class="getTabClass(index)"
      :aria-selected="isActiveTab(index)"
      role="tab"
      :tabindex="getTabIndex(index)"
      type="button"
      @click="handleTabClick(index)"
      @keydown="handleTabKeydown($event, index)"
    >
      <cp-icon v-if="tab.leadingIcon" class="cpTabs__icon" :class="dynamicIconClass" :type="tab.leadingIcon" />
      <cp-text class="cpTabs__label" tag="span" :size="textSize">
        {{ tab.title }}
      </cp-text>
      <div v-if="hasTabCount(tab.count)" class="cpTabs__count">
        <cp-badge :color="dynamicBadgeColor(index)" :variant="dynamicBadgeVariant(index)" :size="badgeSize">
          {{ tab.count }}
        </cp-badge>
      </div>
      <cp-icon v-if="tab.trailingIcon" class="cpTabs__icon" :class="dynamicIconClass" :type="tab.trailingIcon" />
    </button>
    <div ref="activeUnderline" class="cpTabs__activeUnderline" :class="dynamicUnderlineClass" />
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onBeforeUnmount, onMounted, ref, watch } from 'vue'

import { useReducedMotion } from '@/composables/useReducedMotion'

import CpText from '@/components/CpText.vue'

import { capitalizeFirstLetter } from '@/helpers'

type EmitType = {
  (e: 'onTabClick', index: number): void
}

interface Props {
  defaultActiveIndex?: number
  isFullWidth?: boolean
  isLoading?: boolean
  size?: 'xs' | 'sm' | 'md' | 'lg'
  tabs: {
    count?: number
    leadingIcon?: string
    title: string
    trailingIcon?: string
  }[]
  variant?: 'brand' | 'underline'
}

const props = withDefaults(defineProps<Props>(), {
  defaultActiveIndex: 0,
  size: 'md',
  variant: 'underline'
})

const emit = defineEmits<EmitType>()

const cpTabsElement = ref<HTMLElement | null>(null)
const activeUnderline = ref<HTMLElement | null>(null)
const activeTabIndex = ref<number | null>(props.defaultActiveIndex)
const suppressUnderlineTransition = ref(false)
const prefersReducedMotion = useReducedMotion()

const textSize = computed(() => {
  switch (props.size) {
    case 'xs':
      return 'xs'
    case 'sm':
      return 'sm'
    case 'md':
    case 'lg':
    default:
      return 'md'
  }
})

const badgeSize = computed(() => {
  switch (props.size) {
    case 'xs':
    case 'sm':
      return '2xs'
    case 'md':
    case 'lg':
    default:
      return 'xs'
  }
})

const hasTabCount = (count?: number) => typeof count === 'number'

const isActiveTab = (index: number) => activeTabIndex.value === index

const getTabIndex = (index: number) => isActiveTab(index) ? 0 : -1

const activateTab = async (index: number, { focus = false } = {}) => {
  if (props.isLoading) return
  if (index < 0 || index >= props.tabs.length) return

  if (focus) suppressUnderlineTransition.value = true

  activeTabIndex.value = index
  emit('onTabClick', index)
  scrollToActiveElement(focus ? 'instant' : 'smooth')

  if (!focus) return

  await nextTick()
  getActiveTabElement()?.focus()
  requestAnimationFrame(() => (suppressUnderlineTransition.value = false))
}

const handleTabClick = (index: number) => activateTab(index)

const handleTabKeydown = (event: KeyboardEvent, index: number) => {
  if (props.isLoading) return

  const lastIndex = props.tabs.length - 1
  let nextIndex: number | null = null

  switch (event.key) {
    case 'ArrowRight':
      nextIndex = index === lastIndex ? 0 : index + 1
      break
    case 'ArrowLeft':
      nextIndex = index === 0 ? lastIndex : index - 1
      break
    default:
      return
  }

  event.preventDefault()
  activateTab(nextIndex, { focus: true })
}

const dynamicIconClass = computed(() => {
  return { [`cpTabs__icon--${props.size}`]: true }
})

const dynamicTabsClass = computed(() => {
  return {
    [`cpTabs--${props.size}`]: true,
    [`cpTabs--is${capitalizeFirstLetter(props.variant)}`]: true,
    'cpTabs--isFullWidth': props.isFullWidth,
  }
})

const getTabClass = (tabIndex: number) => {
  if (!props.tabs[tabIndex]) return

  return {
    'cpTabs__tab--isActive': activeTabIndex.value === tabIndex,
    'cpTabs__tab--isLoading': props.isLoading,
    [`cpTabs__tab--${props.size}`]: true,
  }
}

const dynamicUnderlineClass = computed(() => {
  return {
    [`cpTabs__activeUnderline--is${capitalizeFirstLetter(props.variant)}`]: true,
    'cpTabs__activeUnderline--noTransition': suppressUnderlineTransition.value
  }
})

const dynamicBadgeColor = (index: number) => index === activeTabIndex.value ? 'accent' : 'neutral'

const dynamicBadgeVariant = (index: number) => index === activeTabIndex.value ? 'solid' : 'soft'

const getActiveTabElement = () => {
  const componentElement = cpTabsElement.value

  if (!componentElement) return
  if (activeTabIndex.value === null) return

  return componentElement.children[activeTabIndex.value] as HTMLElement
}

const getUnderlineElement = () => {
  const underlineElement = activeUnderline.value
  if (!underlineElement) return

  return underlineElement
}

const setUnderlineStyle = () => {
  const activeTabElement = getActiveTabElement()
  const underlineElement = getUnderlineElement()
  const containerElement = cpTabsElement.value

  if (!activeTabElement || !underlineElement || !containerElement) return

  const containerRect = containerElement.getBoundingClientRect()
  const tabRect = activeTabElement.getBoundingClientRect()
  const left = tabRect.left - containerRect.left + containerElement.scrollLeft

  underlineElement.style.width = `${tabRect.width}px`
  underlineElement.style.transform = `translate3d(${left}px, 0, 0)`
}

const scrollToActiveElement = (behavior: ScrollBehavior = 'smooth') => {
  const activeTabElement = getActiveTabElement()
  if (!activeTabElement) return

  return activeTabElement.scrollIntoView({ block: 'nearest', behavior: prefersReducedMotion.value ? 'instant' : behavior })
}

onBeforeUnmount(() => window.removeEventListener('resize', setUnderlineStyle))

onMounted(() => {
  window.addEventListener('resize', setUnderlineStyle)
  nextTick(setUnderlineStyle)
})

watch(activeTabIndex, () => nextTick(setUnderlineStyle))

watch(
  () => props.defaultActiveIndex,
  (newValue) => (activeTabIndex.value = newValue),
)
</script>

<style lang="scss">
.cpTabs {
  @extend %u-scrolling-visual-cue;

  position: relative;
  display: flex;
  width: 100%;
  align-items: center;

  &--xs,
  &--sm {
    gap: var(--cp-spacing-xs);
  }

  &--md,
  &--lg {
    gap: var(--cp-spacing-sm);
  }

  &__activeUnderline {
    position: absolute;
    z-index: 0;
    transition:
      background-color 300ms ease,
      transform 300ms cubic-bezier(0.34, 1.26, 0.64, 1),
      width 300ms cubic-bezier(0.34, 1.26, 0.64, 1);

    &--noTransition {
      transition: none;
    }

    &--isBrand {
      top: auto;
      bottom: auto;
      height: 100%;
      border-radius: var(--cp-radius-md);
    }

    &--isUnderline {
      bottom: 0;
      left: 0;
      height: var(--cp-dimensions-0_5);
      margin-bottom: -1px;
    }
  }

  &__tab {
    @extend %u-focus-outline;

    position: relative;
    z-index: 1;
    display: flex;
    align-items: center;
    justify-content: center;
    border-radius: var(--cp-radius-md);
    color: var(--cp-text-secondary);
    -webkit-tap-highlight-color: transparent;
    transition:
      box-shadow 300ms ease,
      opacity 300ms ease;

    &:focus-visible {
      outline-offset: 0;
    }

    &--isLoading {
      opacity: 0.5;
      pointer-events: none;
    }

    &--isActive {
      color: var(--cp-text-accent-primary);
    }

    &--isActive ~ .cpTabs__activeUnderline--isBrand {
      background-color: var(--cp-background-accent-secondary);
    }

    &--isActive ~ .cpTabs__activeUnderline--isUnderline {
      background-color: var(--cp-background-accent-solid);
    }

    &--xs {
      padding: var(--cp-spacing-sm-md);
      gap: var(--cp-spacing-xs);
    }

    &--sm {
      padding: var(--cp-spacing-sm-md) var(--cp-spacing-md);
      gap: var(--cp-spacing-sm);
    }

    &--md {
      padding: var(--cp-spacing-md) var(--cp-spacing-lg);
      gap: var(--cp-spacing-sm-md);
    }

    &--lg {
      padding: var(--cp-spacing-lg) var(--cp-spacing-xl);
      gap: var(--cp-spacing-sm-md);
    }
  }

  &--isUnderline {
    border-bottom: 1px solid var(--cp-border-soft);

    .cpTabs__tab {
      margin-bottom: -1px;
    }

    .cpTabs__tab:focus-visible {
      border-bottom-left-radius: 0;
      border-bottom-right-radius: 0;
      outline: none;

      &::before {
        position: absolute;
        border: var(--cp-dimensions-0_5) solid var(--cp-border-accent-solid);
        border-radius: inherit;
        border-bottom: none;
        content: '';
        inset: 0;
        pointer-events: none;
      }

      &.cpTabs__tab--isActive::before {
        border-bottom: var(--cp-dimensions-0_5) solid var(--cp-border-accent-solid);
      }
    }

    .cpTabs__tab--isActive:focus-visible ~ .cpTabs__activeUnderline--isUnderline {
      opacity: 0;
    }
  }

  &--isFullWidth {
    .cpTabs__tab {
      flex: 1;
    }
  }

  &__label {
    transition: color 300ms ease;
    white-space: nowrap;
  }

  &__label--xs {
    font-size: var(--cp-text-size-xs);
    line-height: var(--cp-line-height-xs);
  }

  &__label--sm {
    font-size: var(--cp-text-size-sm);
    line-height: var(--cp-line-height-sm);
  }

  &__label--md {
    font-size: var(--cp-text-size-md);
    line-height: var(--cp-line-height-md);
  }

  &__label--lg {
    font-size: var(--cp-text-size-lg);
    line-height: var(--cp-line-height-lg);
  }

  &__count {
    font-variant: tabular-nums;
  }

  &__icon {
    position: relative;
    display: flex;
    border-radius: var(--cp-radius-sm);

    &--xs,
    &--sm {
      @include mx.square-sizing(16);
    }

    &--md,
    &--lg {
      @include mx.square-sizing(20);
    }
  }
}
</style>
