<template>
  <div
    :class="[
      cn.b(),
      cn.m(nativeType),
      cn.is('invalid', invalid),
      cn.is('disabled', disabled),
      cn.is('focused', focused),
      cn.is('readonly', readonly),
      cn.has('value', isFilled),
    ]"
  >
    <label :class="[cn.e('wrapper')]">
      <div :class="[cn.e('content')]">
        <input
          ref="inputRef"
          :inputmode="inputmode"
          :name="id"
          :class="[cn.e('control')]"
          :type="inputType"
          :autocomplete="autocomplete"
          :disabled="disabled"
          :value="inputValue"
          :readonly="readonly"
          :tabindex="readonly ? -1 : 0"
          @input="handleInput"
          @focus="onFocus"
          @blur="onBlur"
          @change="onChange"
        >

        <p :class="[cn.e('label')]">{{ label }}</p>

        <div
          v-if="renderSuffixSlot"
          :class="[cn.e('suffix')]"
        >
          <slot name="suffix">
            <UiButton
              v-if="showPasswordToggler"
              type="ghost"
              color="secondary"
              size="small"
              :icon="showPassword ? 'non-visibility' : 'visibility'"
              @click="togglePassword"
              @mousedown.prevent
            />

            <UiButton
              v-else-if="clearable"
              icon="cross"
              type="ghost"
              color="secondary"
              size="small"
              @click="handleChange('', false)"
              @mousedown.prevent
            />

            <UiButton
              v-else-if="copyable"
              type="ghost"
              color="secondary"
              size="small"
              icon="s-copy"
              @click="copy"
              @mousedown.prevent
            />
          </slot>
        </div>
      </div>
    </label>

    <div
      v-if="!readonly && (!!rules || !!caption || $slots.caption)"
      :class="[cn.e('bottom')]"
    >
      <span
        v-if="invalid"
        :class="[cn.e('validation-message')]"
      >
        {{ errorMessage }}
      </span>

      <slot
        v-else-if="caption || $slots.caption"
        name="caption"
      >
        <span :class="[cn.e('caption')]">
          {{ caption }}
        </span>
      </slot>
    </div>
  </div>
</template>

<script setup lang="ts">
import { useField } from 'vee-validate'
import IMask from 'imask'
import { isEqual } from 'lodash-es'

import type { InputHTMLAttributes } from 'vue'
import type { RuleExpression } from 'vee-validate'
import type { FactoryArg as IMaskOptions, InputMask } from 'imask'

export interface Props {
  id: string
  nativeType?: 'text' | 'password'
  label: string
  disabled?: boolean
  caption?: string
  copyable?: boolean
  clearable?: boolean
  autofocus?: boolean
  readonly?: boolean
  autocomplete?: InputHTMLAttributes['autocomplete']
  inputmode?: InputHTMLAttributes['inputmode']
  rules?: RuleExpression<string>
  mask?: IMaskOptions
  modelValue?: string
}

defineOptions({
  name: 'UiInput',
})

const props = withDefaults(defineProps<Props>(), {
  nativeType: 'text',
  disabled: false,
  copyable: false,
  clearable: false,
  autofocus: false,
  readonly: false,
  autocomplete: 'off',
  inputmode: 'text',
})

defineEmits<{
  'update:modelValue': []
  focus: []
  blur: []
}>()

function isEmptyValue(value: string) {
  return [null, undefined, ''].includes(value)
}

const slots = useSlots()

const cn = useClassname('ui-input')

const { value, errorMessage, setValue, handleChange, handleBlur } = useField(
  toRef(props, 'id'),
  toRef(props, 'rules'),
  {
    validateOnValueUpdate: false,
    initialValue: !isEmptyValue(props.modelValue)
      ? props.modelValue
      : undefined,
    syncVModel: true,
  },
)

const inputRef = ref(null)
const maskInstance = shallowRef<InputMask<IMaskOptions>>()
const maskOptions = toRef(props, 'mask')
const showPassword = ref(false)
const focused = ref(false)
const maskedValue = ref()

let valueEvent = false
let acceptEvent = false
let updateOptionsEvent = false

const active = computed(() => !props.disabled)
const invalid = computed(() => active.value && !!errorMessage.value)

const inputValue = computed(() => {
  return maskInstance.value ? maskedValue.value : value.value
})
const isFilled = computed(() => !isEmptyValue(inputValue.value))

const isPassword = computed(() => props.nativeType === 'password')
const showPasswordToggler = computed(() => isPassword.value && active.value)
const copyable = computed(() => {
  if (isPassword.value)
    return false

  return props.copyable && isFilled.value
})
const clearable = computed(
  () => active.value && props.clearable && isFilled.value,
)

const renderSuffixSlot = computed(() => {
  return (
    slots.suffix
    || showPasswordToggler.value
    || copyable.value
    || clearable.value
  )
})

const inputType = computed(() => {
  return !isPassword.value || showPassword.value ? 'text' : props.nativeType
})

function togglePassword() {
  showPassword.value = !showPassword.value
}

function focus() {
  inputRef.value?.focus()
}

function handleInput(event: Event) {
  if (!maskInstance.value)
    handleChange(event, !!errorMessage.value)
}

function initializeMask() {
  if (props.mask) {
    if (!inputRef.value)
      return
    if (maskInstance.value)
      destroyMask()

    maskInstance.value = IMask(inputRef.value, props.mask).on('accept', () => {
      if (valueEvent || updateOptionsEvent)
        return

      acceptEvent = true

      handleChange(maskInstance.value.unmaskedValue, !!errorMessage.value)
      maskedValue.value = maskInstance.value.value

      nextTick(() => (acceptEvent = false))
    })

    maskInstance.value.unmaskedValue = value.value?.toString() || ''
    maskedValue.value = maskInstance.value.value
  }
}

function destroyMask() {
  maskInstance.value?.destroy()
  maskInstance.value = null
}

function onChange(event: Event) {
  if (!errorMessage.value)
    handleChange(maskInstance.value ? maskInstance.value.unmaskedValue : event)
}

function copy() {
  navigator.clipboard?.writeText(inputValue.value)
}

watch(value, (value) => {
  if (maskInstance.value) {
    if (acceptEvent)
      return

    valueEvent = true
    maskInstance.value.unmaskedValue = value?.toString() || ''
    maskedValue.value = maskInstance.value.value

    nextTick(() => (valueEvent = false))
  }
})

watch(maskOptions, (newOptions, prevValue) => {
  if (newOptions) {
    if (!maskInstance.value) {
      initializeMask()
    }
    else {
      if (isEqual(newOptions, prevValue))
        return

      updateOptionsEvent = true

      maskInstance.value.updateOptions(toRaw(newOptions))
      maskInstance.value.unmaskedValue = value.value?.toString() || ''
      maskedValue.value = maskInstance.value.value

      nextTick(() => (updateOptionsEvent = false))
    }
  }
  else {
    destroyMask()
  }
})

function onFocus(e: Event) {
  focused.value = true
}

function onBlur(e: Event) {
  focused.value = false

  handleBlur(e)
}

onMounted(async () => {
  if (!isEmptyValue(props.modelValue) && !props.disabled)
    setValue(props.modelValue)

  initializeMask()

  await nextTick()

  if (!maskInstance.value)
    setValue(inputRef.value.value, false)

  if (props.autofocus)
    focus()
})

onUnmounted(destroyMask)

defineExpose({
  focused,
  maskInstance,
})
</script>

<style lang="scss">
@use 'styles';
</style>
