<template>
  <div :aria-disabled="isDisabled" class="cpInput" :class="dynamicClasses" @click="focusOnInput">
    <base-input-label
      v-if="label"
      v-bind-once="{ for: inputIdentifier }"
      :is-invalid="isInputInvalid"
      :required="isRequired"
      :tooltip="tooltip"
    >
      {{ capitalizedLabel }}
    </base-input-label>
    <div
      ref="cpInputContainer"
      class="cpInput__container"
      :class="{ 'cpInput__container--hasBeforeIcon': hasBeforeIcon }"
    >
      <div v-if="hasBeforeIcon" class="cpInput__icon cpInput__icon--isBefore">
        <cp-icon v-if="isSearch" type="search" />
        <cp-icon v-else-if="isEmail" type="mail" />
        <slot v-else name="leading-icon" />
      </div>
      <input
        v-model="inputModel"
        v-bind-once="{ id: inputIdentifier }"
        v-maska
        v-bind="restAttributes"
        :aria-describedby="inputDescribedByAttribute"
        :aria-invalid="isInputInvalid"
        class="cpInput__inner"
        :data-maska="mask"
        :disabled="isDisabled"
      />
      <div v-if="hasAfterIcon" class="cpInput__icon cpInput__icon--isAfter">
        <slot v-if="displayAfterIcon" name="trailing-icon" />
        <transition name="fade">
          <button v-if="isClearButtonVisible" class="cpInput__clear" type="button" @click="clearInputValue">
            <cp-icon class="cpInput__clearIcon" type="x" />
          </button>
        </transition>
      </div>
    </div>
    <cp-transition-expand mode="out-in">
      <p v-if="displayErrorMessage" :id="errorMessageId" class="cpInput__error">
        {{ errorMessage }}
      </p>
      <p v-else-if="displayHelp" :id="helpMessageId" class="cpInput__help">
        {{ help }}
      </p>
    </cp-transition-expand>
  </div>
</template>

<script setup lang="ts">
import { computed, nextTick, onMounted, ref, useAttrs, useId, useSlots } from 'vue'

import type { Sizes } from '@/constants'

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

import { capitalizeFirstLetter } from '@/helpers'

interface Emits {
  (e: 'update:modelValue', value: string | number | boolean): void
}

export interface Props {
  errorMessage?: string
  help?: string
  inputId?: string | null
  isInvalid?: boolean
  isSearch?: boolean
  label?: string
  mask?: string | Record<string, unknown> | null
  modelValue?: string | number | boolean
  removeBorder?: boolean
  size?: Sizes
  tooltip?: string
}

const props = withDefaults(defineProps<Props>(), {
  errorMessage: '',
  label: '',
  help: '',
  inputId: null,
  mask: null,
  modelValue: '',
  size: 'md',
  tooltip: '',
})

const emit = defineEmits<Emits>()

defineOptions({ inheritAttrs: false })

const attrs = useAttrs()

// class is a reserved word, we can't remove 'class' property from attrs
const { class: _class, id, ...restAttributes } = attrs

const inputIdentifier = ref<string>((id as string | undefined) || useId())

const helpMessageId = useId()
const errorMessageId = useId()

const inputDescribedByAttribute = computed(() => `${helpMessageId} ${errorMessageId}`)

const slots = useSlots()

const capitalizedLabel = computed(() => capitalizeFirstLetter(props.label))

const inputModel = defineModel<string | number | boolean>({
  type: [String, Number, Boolean],
  default: '',
  set(newValue) {
    handleChange(newValue)
    return newValue
  },
})

const isDOMElementValid = ref(true)
const cpInputContainer = ref<HTMLDivElement | null>(null)

const isDisabled = computed(() => checkAttribute('disabled'))
const isRequired = computed(() => checkAttribute('required'))

const isEmail = computed(() => attrs.type === 'email')

const dynamicClasses = computed(() => {
  return [
    attrs.class,
    `cpInput--${props.size}`,
    {
      'cpInput--isInvalid': isInputInvalid.value,
      'cpInput--isDisabled': isDisabled.value,
      'cpInput--hasNoBorder': props.removeBorder,
      'cpInput--isSearch': props.isSearch,
    },
  ]
})

const isInputInvalid = computed(() => props.isInvalid || !isDOMElementValid.value)

const hasBeforeIconSlot = computed(() => !!slots['leading-icon'])
const hasBeforeIcon = computed(() => hasBeforeIconSlot.value || props.isSearch || isEmail.value)

const hasAfterIconSlot = computed(() => !!slots['trailing-icon'])
const hasAfterIcon = computed(() => hasAfterIconSlot.value || props.isSearch)
const displayAfterIcon = computed(() => hasAfterIcon.value && !props.isSearch)

const DOMElement = computed(
  () => cpInputContainer.value?.children.namedItem(inputIdentifier.value) as HTMLInputElement | null,
)

const displayErrorMessage = computed(() => isInputInvalid.value && props.errorMessage.length)
const isClearButtonVisible = computed(() => props.isSearch && inputModel.value.toString().length && !isDisabled.value)

const displayHelp = computed(() => props.help?.length && !displayErrorMessage.value)

const handleChange = (newValue: string | number | boolean) => {
  emit('update:modelValue', newValue)
  checkInputValidity()
}

const focusOnInput = () => {
  if (!DOMElement.value) return
  DOMElement.value.focus()
}

const checkInputValidity = () => {
  if (!DOMElement.value) return false

  isDOMElementValid.value =
    (DOMElement.value.validity && DOMElement.value.validity.valid) ||
    (DOMElement.value.validity && DOMElement.value.validity.valueMissing)
}

const checkAttribute = (attribute: string) => attrs[attribute] === '' || attrs[attribute] === true

const clearInputValue = () => emit('update:modelValue', '')

onMounted(async () => {
  await nextTick()
  checkInputValidity()
})
</script>

<style lang="scss">
// Main
.cpInput {
  position: relative;
  display: flex;
  flex-direction: column;

  &__container {
    position: relative;
    z-index: 1;
    display: flex;

    * {
      cursor: text;
    }
  }

  &:has(.cpInput__help, .cpInput__error) .cpInput__container {
    margin-bottom: var(--cp-spacing-md);
  }

  &:has(.v-popper__popper--shown) {
    z-index: 3;
  }

  &__inner {
    width: 100%;
    padding: var(--cp-spacing-md) var(--cp-spacing-xl);
    border-radius: var(--cp-radius-md);
    appearance: none;
    box-shadow:
      var(--cp-shadows-3xs-inset),
      0 0 0 var(--cp-dimensions-0_25) var(--cp-border-soft);
    color: inherit;
    font-size: var(--cp-text-size-sm);
    line-height: var(--cp-line-height-md);

    &:hover {
      box-shadow:
        var(--cp-shadows-3xs-inset),
        0 0 0 var(--cp-dimensions-0_25) var(--cp-border-strong-hover);
    }

    &:focus {
      background-color: var(--cp-background-primary);
      outline: var(--cp-dimensions-0_5) solid var(--cp-focus);
    }

    &::placeholder {
      color: var(--cp-text-placeholder);
    }
  }

  &--hasNoBorder .cpInput__inner,
  &--hasNoBorder .cpInput__inner:hover,
  &--hasNoBorder .cpInput__inner:focus,
  &--hasNoBorder .cpInput__icon:hover ~ .cpInput__inner {
    box-shadow: none;
    outline: transparent;
  }

  &--isInvalid {
    .cpInput__inner,
    .cpInput__inner:hover,
    .cpInput__inner:focus,
    .cpInput__icon:hover ~ .cpInput__inner {
      box-shadow:
        var(--cp-shadows-3xs-inset),
        0 0 0 var(--cp-dimensions-0_25) var(--cp-border-error-solid);
    }

    .cpInput__inner:focus {
      outline: var(--cp-dimensions-0_5) solid var(--cp-border-error-solid);
    }
  }

  &--isDisabled {
    color: var(--cp-text-disabled);

    .cpInput__container * {
      cursor: not-allowed;
    }

    .cpInput__inner {
      background: var(--cp-background-disabled);

      &::placeholder {
        color: var(--cp-text-disabled);
      }
    }

    .cpInput__inner,
    .cpInput__inner:hover {
      box-shadow: 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-disabled);
    }

    .cpInput__icon {
      color: var(--cp-foreground-disabled);
    }

    .cpInput__icon:hover ~ .cpInput__inner {
      box-shadow: 0 0 0 var(--cp-dimensions-0_25) var(--cp-border-disabled);
    }
  }

  &__icon {
    position: absolute;
    z-index: 1;
    top: 50%;
    left: var(--cp-dimensions-3_5);
    display: flex;
    overflow: hidden;
    width: var(--cp-dimensions-4);
    height: var(--cp-dimensions-4);
    flex-shrink: 0;
    align-items: center;
    color: var(--cp-foreground-primary);
    transform: translateY(-50%);

    &:hover ~ .cpInput__inner {
      box-shadow: 0 0 0 var(--cp-dimensions-0_25) var(--cp-foreground-accent-primary);
    }

    svg {
      width: 100%;
      height: 100%;
      stroke-width: 1.6;
    }

    &--isBefore,
    &--isAfter {
      font-size: var(--cp-text-size-sm);
    }

    &--isAfter {
      right: var(--cp-dimensions-3_5);
      left: auto;
    }

    &--isBefore ~ input {
      padding-left: calc(var(--cp-spacing-lg) + var(--cp-spacing-sm) + var(--cp-dimensions-4) + var(--cp-spacing-md));
    }
  }

  &:has(.cpInput__icon--isAfter) input {
    padding-right: calc(var(--cp-spacing-lg) + var(--cp-spacing-sm) + var(--cp-dimensions-4) + var(--cp-spacing-md));
  }

  &--lg {
    .cpInput__inner {
      padding: var(--cp-spacing-lg) var(--cp-spacing-xl);
      border-radius: var(--cp-radius-lg);
      font-size: var(--cp-text-size-md);
    }

    .cpInput__icon {
      left: var(--cp-spacing-lg);
      width: var(--cp-dimensions-5);
      height: var(--cp-dimensions-5);
    }

    .cpInput__icon--isBefore,
    .cpInput__icon--isAfter {
      font-size: var(--cp-text-size-md);
    }

    .cpInput__icon--isAfter {
      right: var(--cp-spacing-lg);
      left: auto;
    }

    .cpInput__icon--isAfter ~ input {
      padding-right: calc(var(--cp-spacing-xl) + var(--cp-dimensions-5) + var(--cp-spacing-sm));
    }

    .cpInput__icon--isBefore ~ input {
      padding-left: calc(var(--cp-spacing-xl) + var(--cp-dimensions-5) + var(--cp-spacing-sm));
    }
  }

  &--sm {
    .cpInput__inner {
      padding: var(--cp-spacing-sm) var(--cp-spacing-lg);
      border-radius: var(--cp-radius-md);
      font-size: var(--cp-text-size-sm);
    }

    .cpInput__icon {
      left: var(--cp-dimensions-2_5);
    }

    .cpInput__icon--isBefore,
    .cpInput__icon--isAfter {
      font-size: var(--cp-text-size-xs);
    }

    .cpInput__icon--isAfter {
      right: var(--cp-dimensions-2_5);
      left: auto;
    }

    .cpInput__icon--isBefore ~ input {
      padding-left: calc(var(--cp-spacing-md) + var(--cp-dimensions-5) + var(--cp-spacing-md));
    }

    .cpInput__icon--isAfter ~ input {
      padding-right: calc(var(--cp-spacing-md) + var(--cp-dimensions-5) + var(--cp-spacing-md));
    }
  }

  // If icon slot is text (units)
  .cpInput__icon:not(:has(.cpIcon)) {
    width: auto;
    height: auto;
    aspect-ratio: auto;
  }

  &--isSearch {
    .cpInput__icon--isAfter {
      overflow: visible;
      width: auto;
      height: auto;
    }

    .cpInput__clear {
      display: flex;
      width: var(--cp-dimensions-6);
      height: var(--cp-dimensions-6);
      padding: var(--cp-spacing-sm);
      border-radius: var(--cp-radius-full);
      background-color: transparent;
      color: var(--cp-foreground-secondary);

      &:hover {
        background-color: var(--cp-background-primary-hover);
        color: var(--cp-foreground-secondary-hover);
      }

      &:focus-visible {
        outline: var(--cp-dimensions-0_5) solid var(--cp-focus);
        outline-offset: var(--cp-dimensions-0_25);
      }
    }

    .cpInput__clearIcon {
      width: var(--cp-dimensions-4);
      height: var(--cp-dimensions-4);
      pointer-events: none;
    }

    &.cpInput--lg .cpInput__clearIcon {
      width: var(--cp-dimensions-4);
      height: var(--cp-dimensions-4);
    }
  }

  &__help,
  &__error {
    font-size: var(--cp-text-size-sm);
    line-height: var(--cp-line-height-md);
  }

  &__help {
    color: var(--cp-text-secondary);
  }

  &__error {
    color: var(--cp-text-error-primary);

    &::first-letter {
      text-transform: capitalize;
    }
  }
}
</style>
