import { computed, defineComponent, h, ref, useId, PropType, type VNode } from 'vue'

import { chipsFromData } from '../chip-set/buildChips'
import { useChipSet, type ChipSetConfig } from '../chip-set/useChipSet'
import { isRTL } from '../../utils'

type ChipClassName = string | ((value: string) => string)

// Initial chip values can be supplied declaratively as CChip slot content
// (parity with the vanilla ChipInput, which reads existing .chip elements).
const slotText = (slot: unknown): string | undefined => {
  if (typeof slot !== 'function') {
    return undefined
  }

  const rendered = slot()
  const first = Array.isArray(rendered) ? rendered[0] : rendered
  if (typeof first === 'string') {
    return first
  }

  return typeof first?.children === 'string' ? first.children : undefined
}

const valuesFromSlot = (nodes: VNode[] = []): string[] => {
  const values: string[] = []
  for (const node of nodes) {
    if (Array.isArray(node.children)) {
      values.push(...valuesFromSlot(node.children as VNode[]))
      continue
    }

    const value =
      (node.props?.value as string | undefined) ??
      slotText((node.children as { default?: unknown } | null)?.default)
    if (value) {
      values.push(value)
    }
  }

  return values
}

const uniqueValues = (values: string[]): string[] => [
  ...new Set(values.map((value) => value.trim()).filter(Boolean)),
]

const resolveChipClassName = (
  chipClassName: ChipClassName | undefined,
  value: string
): string | undefined => {
  if (!chipClassName) {
    return undefined
  }

  if (typeof chipClassName === 'function') {
    const resolvedClassName = chipClassName(value)
    return typeof resolvedClassName === 'string' ? resolvedClassName : undefined
  }

  return chipClassName
}

const CChipInput = defineComponent({
  name: 'CChipInput',
  props: {
    /**
     * Adds custom classes to chips rendered by the component. Accepts a static className or a resolver function based on chip value.
     */
    chipClassName: {
      type: [String, Function] as PropType<ChipClassName>,
      default: undefined,
    },
    /**
     * Creates a new chip when the component loses focus with a pending value.
     */
    createOnBlur: {
      type: Boolean,
      default: true,
    },
    /**
     * Sets the initial uncontrolled values rendered by the component.
     */
    defaultValue: {
      type: Array as PropType<string[]>,
      default: () => [],
    },
    /**
     * Toggle the disabled state for the component.
     */
    disabled: Boolean,
    /**
     * Renders the chips as filter chips, each showing a leading check icon while selected. Implies `selectable`.
     */
    filter: Boolean,
    /**
     * Sets the `id` of the internal text input rendered by the component.
     */
    id: String,
    /**
     * Renders an inline label inside the component container.
     */
    label: [String, Object],
    /**
     * Sets the maximum number of chips that can be created in the component.
     */
    maxChips: {
      type: Number,
      default: null,
    },
    /**
     * The default name for a value passed using v-model.
     */
    modelValue: {
      type: Array as PropType<string[]>,
      default: undefined,
    },
    /**
     * Sets the name of the hidden input used by the component for form submission.
     */
    name: String,
    /**
     * Sets placeholder text for the internal input of the component.
     */
    placeholder: {
      type: String,
      default: '',
    },
    /**
     * Toggle the readonly state for the component.
     */
    readOnly: Boolean,
    /**
     * Displays remove buttons on chips managed by the component.
     */
    removable: {
      type: Boolean,
      default: true,
    },
    /**
     * Enables chip selection behavior in the component.
     */
    selectable: Boolean,
    /**
     * Sets how many chips can be selected at once.
     *
     * @values 'single', 'multiple'
     */
    selectionMode: {
      type: String as PropType<'single' | 'multiple'>,
      default: 'multiple',
    },
    /**
     * Sets the separator character used to create chips while typing or pasting in the component.
     */
    separator: {
      type: String,
      default: ',',
    },
    /**
     * Size the component small or large.
     *
     * @values 'sm', 'lg'
     */
    size: {
      type: String,
      validator: (value: string) => {
        return ['sm', 'lg'].includes(value)
      },
    },
  },
  emits: [
    /**
     * Event occurs when the component adds a new chip.
     */
    'add',
    /**
     * Event occurs when the value list changes.
     */
    'change',
    /**
     * Event occurs when the internal text input value changes.
     */
    'input',
    /**
     * Event occurs when the component removes a chip.
     */
    'remove',
    /**
     * Event occurs when the selected chip values change.
     */
    'select',
    /**
     * Emit the new value whenever there's a change.
     */
    'update:modelValue',
  ],
  setup(props, { attrs, emit, expose, slots }) {
    const internalValues = ref<string[]>(
      uniqueValues(
        props.defaultValue.length > 0 ? props.defaultValue : valuesFromSlot(slots.default?.())
      )
    )
    const inputValue = ref('')
    const inputRef = ref<HTMLInputElement>()
    const generatedName = useId()

    const values = computed(() =>
      props.modelValue === undefined
        ? uniqueValues(internalValues.value)
        : uniqueValues(props.modelValue as string[])
    )

    // CChipInput builds on the same engine as CChipSet: useChipSet owns selection
    // coordination, roving focus, and chip prop forwarding (provided to the chips
    // below). CChipInput owns the chip list and adds the text-input layer.
    const config = computed<ChipSetConfig>(() => ({
      disabled: props.disabled,
      filter: props.filter,
      removable: Boolean(props.removable && !props.disabled && !props.readOnly),
      selectable: props.selectable,
    }))

    const { rootRef, clearSelection, getFocusableChips, handleKeydown } = useChipSet({
      config,
      selectionMode: () => props.selectionMode,
      selected: () => undefined,
      restoreFocusOnRemove: false,
      onSelectionChange: (selected) => emit('select', selected),
      onRemove: (value) => remove(value),
    })

    const emitValuesChange = (nextValues: string[]): void => {
      if (props.modelValue === undefined) {
        internalValues.value = nextValues
      }

      emit('update:modelValue', nextValues)
      emit('change', nextValues)
    }

    const canAddMore = computed(
      () => props.maxChips === null || values.value.length < props.maxChips
    )

    const add = (rawValue: string): boolean => {
      if (props.disabled || props.readOnly) {
        return false
      }

      const normalizedValue = String(rawValue).trim()
      if (!normalizedValue || values.value.includes(normalizedValue) || !canAddMore.value) {
        return false
      }

      const nextValues = [...values.value, normalizedValue]
      emitValuesChange(nextValues)
      emit('add', normalizedValue)
      return true
    }

    const remove = (valueToRemove: string): boolean => {
      if (props.disabled || props.readOnly) {
        return false
      }

      if (!values.value.includes(valueToRemove)) {
        return false
      }

      // Selection is cleaned up by useChipSet; here we just drop the value.
      emitValuesChange(values.value.filter((item) => item !== valueToRemove))
      emit('remove', valueToRemove)
      inputRef.value?.focus()
      return true
    }

    const createFromInput = (): void => {
      if (add(inputValue.value)) {
        inputValue.value = ''
      }
    }

    const focusLastChip = (): void => {
      const chips = getFocusableChips()
      chips[chips.length - 1]?.focus()
    }

    const handleInputKeydown = (event: KeyboardEvent): void => {
      switch (event.key) {
        case 'Enter': {
          event.preventDefault()
          createFromInput()
          break
        }

        case 'Backspace':
        case 'Delete': {
          if (inputValue.value === '') {
            event.preventDefault()
            focusLastChip()
          }
          break
        }

        case 'ArrowLeft':
        case 'ArrowRight': {
          // The arrow pointing toward the chips (left in LTR, right in RTL) jumps
          // to the last chip when the caret is at the start of the input.
          const towardChipsKey = isRTL(rootRef.value) ? 'ArrowRight' : 'ArrowLeft'
          const target = event.currentTarget as HTMLInputElement
          if (
            event.key === towardChipsKey &&
            target.selectionStart === 0 &&
            target.selectionEnd === 0
          ) {
            event.preventDefault()
            focusLastChip()
          }
          break
        }

        case 'Escape': {
          inputValue.value = ''
          ;(event.currentTarget as HTMLInputElement).blur()
          break
        }

        // No default
      }
    }

    const handleInputChange = (value: string): void => {
      if (props.disabled || props.readOnly) {
        return
      }

      if (props.separator && value.includes(props.separator)) {
        const parts = value.split(props.separator)
        const chipsToAdd = uniqueValues(parts.slice(0, -1))

        const newChips = chipsToAdd.filter((chip) => !values.value.includes(chip))
        const availableSlots =
          props.maxChips === null ? Infinity : props.maxChips - values.value.length
        const chipsToEmit = newChips.slice(0, availableSlots)

        if (chipsToEmit.length > 0) {
          const nextValues = [...values.value, ...chipsToEmit]
          chipsToEmit.forEach((chip) => emit('add', chip))
          emitValuesChange(nextValues)
        }

        const tail = parts[parts.length - 1] || ''
        inputValue.value = tail
        emit('input', tail)
        return
      }

      inputValue.value = value
      emit('input', value)
    }

    const handlePaste = (event: ClipboardEvent): void => {
      if (props.disabled || props.readOnly || !props.separator) {
        return
      }

      const pastedData = event.clipboardData?.getData('text')
      if (!pastedData?.includes(props.separator)) {
        return
      }

      event.preventDefault()
      const chipsToAdd = uniqueValues(pastedData.split(props.separator))

      const newChips = chipsToAdd.filter((chip) => !values.value.includes(chip))
      const availableSlots =
        props.maxChips === null ? Infinity : props.maxChips - values.value.length
      const chipsToEmit = newChips.slice(0, availableSlots)

      if (chipsToEmit.length > 0) {
        const nextValues = [...values.value, ...chipsToEmit]
        chipsToEmit.forEach((chip) => emit('add', chip))
        emitValuesChange(nextValues)
      }

      inputValue.value = ''
      emit('input', '')
    }

    const handleInputBlur = (event: FocusEvent): void => {
      if (!props.createOnBlur) {
        return
      }

      if ((event.relatedTarget as HTMLElement | null)?.closest('.chip')) {
        return
      }

      createFromInput()
    }

    const handleContainerKeydown = (event: KeyboardEvent): void => {
      if (event.target === inputRef.value) {
        return
      }

      // The arrow past the last chip moves focus into the text field (mirrored in RTL).
      if (event.key === (isRTL(rootRef.value) ? 'ArrowLeft' : 'ArrowRight')) {
        const chips = getFocusableChips()
        const lastChip = chips[chips.length - 1]
        if (lastChip?.contains(event.target as Node)) {
          event.preventDefault()
          inputRef.value?.focus()
          return
        }
      }

      if (handleKeydown(event)) {
        return
      }

      if (event.key.length === 1) {
        inputRef.value?.focus()
      }
    }

    const handleContainerClick = (event: MouseEvent): void => {
      if (event.target === rootRef.value) {
        inputRef.value?.focus()
      }
    }

    expose({ rootRef, inputRef })

    return () => {
      const inputSize = Math.max(props.placeholder.length, inputValue.value.length, 1)

      const children = [
        props.label &&
          h(
            'label',
            {
              class: 'chip-input-label',
              for: props.id,
            },
            props.label
          ),
        ...chipsFromData(
          values.value.map((chipValue) => ({
            value: chipValue,
            label: chipValue,
            ariaRemoveLabel: `Remove ${chipValue}`,
            class: resolveChipClassName(props.chipClassName, chipValue),
          }))
        ),
        h('input', {
          ref: inputRef,
          type: 'text',
          id: props.id,
          class: 'chip-input-field',
          disabled: props.disabled,
          readonly: Boolean(!props.disabled && props.readOnly),
          placeholder: props.placeholder,
          size: inputSize,
          value: inputValue.value,
          onBlur: handleInputBlur,
          onInput: (event: Event) => handleInputChange((event.target as HTMLInputElement).value),
          onKeydown: handleInputKeydown,
          onPaste: handlePaste,
          onFocus: clearSelection,
        }),
        h('input', {
          type: 'hidden',
          name: props.name ?? generatedName,
          value: values.value.join(','),
        }),
      ].filter(Boolean)

      return h(
        'div',
        {
          ref: rootRef,
          class: [
            'chip-input',
            {
              [`chip-input-${props.size}`]: props.size,
              disabled: props.disabled,
            },
            attrs.class,
          ],
          'aria-disabled': props.disabled ? true : undefined,
          'aria-readonly': props.readOnly ? true : undefined,
          onClick: handleContainerClick,
          onKeydown: handleContainerKeydown,
        },
        children
      )
    }
  },
})

export { CChipInput }
