import {
  computed,
  defineComponent,
  h,
  nextTick,
  onUnmounted,
  provide,
  PropType,
  ref,
  Ref,
  watch,
} from 'vue'
import type { Placement } from '@popperjs/core'

import { usePopper } from '../../composables'
import type { Triggers } from '../../types'
import { getNextActiveElement, isRTL } from '../../utils'

import type { Alignments } from './types'
import { getPlacement, getReferenceElement } from './utils'
import { CFocusTrap } from '../focus-trap'

const CDropdown = defineComponent({
  name: 'CDropdown',
  props: {
    /**
     * Set aligment of dropdown menu.
     *
     * @values { 'start' | 'end' | { xs: 'start' | 'end' } | { sm: 'start' | 'end' } | { md: 'start' | 'end' } | { lg: 'start' | 'end' } | { xl: 'start' | 'end'} | { xxl: 'start' | 'end'} }
     */
    alignment: {
      type: [String, Object] as PropType<string | Alignments>,
      // eslint-disable-next-line @typescript-eslint/no-explicit-any
      validator: (value: string | any) => {
        if (value === 'start' || value === 'end') {
          return true
        } else {
          if (value.xs !== undefined && (value.xs === 'start' || value.xs === 'end')) {
            return true
          }
          if (value.sm !== undefined && (value.sm === 'start' || value.sm === 'end')) {
            return true
          }
          if (value.md !== undefined && (value.md === 'start' || value.md === 'end')) {
            return true
          }
          if (value.lg !== undefined && (value.lg === 'start' || value.lg === 'end')) {
            return true
          }
          if (value.xl !== undefined && (value.xl === 'start' || value.xl === 'end')) {
            return true
          }
          if (value.xxl !== undefined && (value.xxl === 'start' || value.xxl === 'end')) {
            return true
          }
          return false
        }
      },
    },
    /**
     * Configure the auto close behavior of the dropdown:
     * - `true` - the dropdown will be closed by clicking outside or inside the dropdown menu.
     * - `false` - the dropdown will be closed by clicking the toggle button and manually calling hide or toggle method. (Also will not be closed by pressing esc key)
     * - `'inside'` - the dropdown will be closed (only) by clicking inside the dropdown menu.
     * - `'outside'` - the dropdown will be closed (only) by clicking outside the dropdown menu.
     */
    autoClose: {
      type: [Boolean, String] as PropType<boolean | 'inside' | 'outside'>,
      default: true,
      validator: (value: boolean | string) => {
        return typeof value === 'boolean' || ['inside', 'outside'].includes(value)
      },
    },
    /**
     * Appends the vue dropdown menu to a specific element. You can pass an HTML element or function that returns a single element. By default `document.body`.
     *
     * @since 5.0.0
     */
    container: {
      type: [Object, String] as PropType<HTMLElement | (() => HTMLElement) | string>,
      default: 'body',
    },
    /**
     * Sets a darker color scheme to match a dark navbar.
     */
    dark: Boolean,
    /**
     * Sets a specified  direction and location of the dropdown menu.
     *
     * @values 'center', 'dropup', 'dropup-center', 'dropend', 'dropstart'
     */
    direction: {
      type: String,
      validator: (value: string) => {
        return ['center', 'dropup', 'dropup-center', 'dropend', 'dropstart'].includes(value)
      },
    },
    /**
     * Toggle the disabled state for the component.
     */
    disabled: Boolean,
    /**
     * Offset of the dropdown menu relative to its target.
     *
     * @since 4.9.0
     */
    offset: {
      type: Array,
      default: () => [0, 2],
    },
    /**
     * Describes the placement of your component after Popper.js has applied all the modifiers that may have flipped or altered the originally provided placement property.
     *
     * @values 'auto', 'top-end', 'top', 'top-start', 'bottom-end', 'bottom', 'bottom-start', 'right-start', 'right', 'right-end', 'left-start', 'left', 'left-end'
     */
    placement: {
      type: String as PropType<Placement>,
      default: 'bottom-start',
    },
    /**
     * If you want to disable dynamic positioning set this property to `true`.
     */
    popper: {
      type: Boolean,
      default: true,
    },
    /**
     * Sets the reference element for positioning the Vue Dropdown Menu.
     * - `toggle` - The Vue Dropdown Toggle button (default).
     * - `parent` - The Vue Dropdown wrapper element.
     * - `HTMLElement` - A custom HTML element.
     * - `Ref` - A custom reference element.
     *
     * @since 5.7.0
     */
    reference: {
      type: [String, Object] as PropType<
        'parent' | 'toggle' | HTMLElement | Ref<HTMLElement | null>
      >,
      default: 'toggle',
    },
    /**
     * Generates dropdown menu using Teleport.
     *
     * @since 5.0.0
     */
    teleport: {
      type: Boolean,
      default: false,
    },
    /**
     * Sets which event handlers you’d like provided to your toggle prop. You can specify one trigger or an array of them.
     */
    trigger: {
      type: String as PropType<Triggers>,
      default: 'click',
    },
    /**
     * Set the dropdown variant to an btn-group, dropdown, input-group, and nav-item.
     *
     * @values 'btn-group', 'dropdown', 'input-group', 'nav-item'
     */
    variant: {
      type: String,
      default: 'btn-group',
      validator: (value: string) => {
        return ['btn-group', 'dropdown', 'input-group', 'nav-item'].includes(value)
      },
    },
    /**
     * Toggle the visibility of dropdown menu component.
     */
    visible: Boolean,
  },
  emits: [
    /**
     * Callback fired when the component requests to be hidden.
     */
    'hide',
    /**
     * Callback fired when the component requests to be shown.
     */
    'show',
  ],
  setup(props, { slots, emit }) {
    const dropdownRef = ref<HTMLElement | null>(null)
    const dropdownMenuRef = ref<HTMLElement | null>(null)
    const dropdownToggleRef = ref<HTMLElement | null>(null)
    const pendingKeyDownEventRef = ref<KeyboardEvent | null>(null)
    const popper = ref(typeof props.alignment === 'object' ? false : props.popper)
    const visible = ref(props.visible)

    const { initPopper, destroyPopper } = usePopper()

    const popperConfig = computed(() => ({
      modifiers: [
        {
          name: 'offset',
          options: {
            offset: props.offset,
          },
        },
      ],
      placement: getPlacement(
        props.placement,
        props.direction,
        props.alignment,
        isRTL(dropdownMenuRef.value)
      ) as Placement,
    }))

    watch(
      () => props.visible,
      () => {
        visible.value = props.visible
      }
    )

    watch(visible, () => {
      if (visible.value && dropdownToggleRef.value && dropdownMenuRef.value) {
        const referenceElement = getReferenceElement(
          props.reference,
          dropdownToggleRef,
          dropdownRef
        )
        if (referenceElement && popper.value) {
          initPopper(referenceElement, dropdownMenuRef.value, popperConfig.value)
        }

        window.addEventListener('click', handleClick)
        window.addEventListener('keyup', handleKeyup)
        dropdownToggleRef.value.addEventListener('keydown', handleKeydown)
        dropdownMenuRef.value.addEventListener('keydown', handleKeydown)

        if (pendingKeyDownEventRef.value) {
          nextTick(() => {
            handleKeydown(pendingKeyDownEventRef.value as KeyboardEvent)
            pendingKeyDownEventRef.value = null
          })
        }

        emit('show')
        return
      }

      if (popper.value) {
        destroyPopper()
      }

      window.removeEventListener('click', handleClick)
      window.removeEventListener('keyup', handleKeyup)
      dropdownMenuRef.value && dropdownMenuRef.value.removeEventListener('keydown', handleKeydown)
      dropdownToggleRef.value &&
        dropdownToggleRef.value.removeEventListener('keydown', handleKeydown)
      emit('hide')
    })

    onUnmounted(() => {
      dropdownToggleRef.value &&
        dropdownToggleRef.value.removeEventListener('keydown', handleKeydown)
      dropdownMenuRef.value && dropdownMenuRef.value.removeEventListener('keydown', handleKeydown)
    })

    provide('config', {
      alignment: props.alignment,
      container: props.container,
      dark: props.dark,
      popper: props.popper,
      teleport: props.teleport,
    })

    provide('variant', props.variant)
    provide('visible', visible)
    provide('dropdownToggleRef', dropdownToggleRef)
    provide('dropdownMenuRef', dropdownMenuRef)
    provide('pendingKeyDownEventRef', pendingKeyDownEventRef)

    const handleKeydown = (event: KeyboardEvent) => {
      if (dropdownMenuRef.value && (event.key === 'ArrowDown' || event.key === 'ArrowUp')) {
        event.preventDefault()
        const target = event.target as HTMLElement
        const items: HTMLElement[] = Array.from(
          dropdownMenuRef.value.querySelectorAll('.dropdown-item:not(.disabled):not(:disabled)')
        )
        getNextActiveElement(items, target, event.key === 'ArrowDown', true).focus()
      }
    }

    const handleKeyup = (event: KeyboardEvent) => {
      if (props.autoClose === false) {
        return
      }

      if (event.key === 'Escape') {
        setVisible(false)
        dropdownToggleRef.value?.focus()
      }
    }

    const handleClick = (event: Event) => {
      if (!dropdownToggleRef.value || !dropdownMenuRef.value) {
        return
      }

      if ((event as MouseEvent).button === 2) {
        return
      }

      const composedPath = event.composedPath()
      const isOnToggle = composedPath.includes(dropdownToggleRef.value)
      const isOnMenu = composedPath.includes(dropdownMenuRef.value)

      if (isOnToggle) {
        return
      }

      const target = event.target as HTMLElement | null
      const FORM_TAG_RE = /^(input|select|option|textarea|form)$/i

      if (isOnMenu && target && FORM_TAG_RE.test(target.tagName)) {
        return
      }

      if (
        props.autoClose === true ||
        (props.autoClose === 'inside' && isOnMenu) ||
        (props.autoClose === 'outside' && !isOnMenu)
      ) {
        setVisible(false)
      }
    }

    const setVisible = (_visible?: boolean, event?: KeyboardEvent) => {
      if (props.disabled) {
        return
      }

      if (typeof _visible === 'boolean') {
        if (event) {
          pendingKeyDownEventRef.value = event || null
        }

        visible.value = _visible

        return
      }
    }

    provide('setVisible', setVisible)

    return () =>
      h(
        CFocusTrap,
        { active: props.teleport && visible.value, additionalContainer: dropdownMenuRef },
        () =>
          props.variant === 'input-group'
            ? [slots.default && slots.default()]
            : h(
                'div',
                {
                  class: [
                    props.variant === 'nav-item' ? 'nav-item dropdown' : props.variant,
                    props.direction === 'center'
                      ? 'dropdown-center'
                      : props.direction === 'dropup-center'
                        ? 'dropup dropup-center'
                        : props.direction,
                  ],
                  ref: dropdownRef,
                },
                slots.default && slots.default()
              )
      )
  },
})

export { CDropdown }
