import { nextTick } from 'vue'
import { on, off, obtainAllFocusableElements } from '@element-ultra/utils'
import { EVENT_CODE } from '@element-ultra/shared'
import type { ObjectDirective } from 'vue'

export const FOCUSABLE_CHILDREN = '_trap-focus-children'
export const TRAP_FOCUS_HANDLER = '_trap-focus-handler'

export interface ITrapFocusElement extends HTMLElement {
  [FOCUSABLE_CHILDREN]: HTMLElement[]
  [TRAP_FOCUS_HANDLER]: (e: KeyboardEvent) => void
}

const FOCUS_STACK = []

const FOCUS_HANDLER = (e: KeyboardEvent) => {
  // Getting the top layer.
  if (FOCUS_STACK.length === 0) return
  const focusableElement =
    FOCUS_STACK[FOCUS_STACK.length - 1][FOCUSABLE_CHILDREN]
  if (focusableElement.length > 0 && e.code === EVENT_CODE.tab) {
    if (focusableElement.length === 1) {
      e.preventDefault()
      if (document.activeElement !== focusableElement[0]) {
        focusableElement[0].focus()
      }
      return
    }
    const goingBackward = e.shiftKey
    const isFirst = e.target === focusableElement[0]
    const isLast = e.target === focusableElement[focusableElement.length - 1]
    if (isFirst && goingBackward) {
      e.preventDefault()
      focusableElement[focusableElement.length - 1].focus()
    }
    if (isLast && !goingBackward) {
      e.preventDefault()
      focusableElement[0].focus()
    }

    // the is critical since jsdom did not implement user actions, you can only mock it
    // DELETE ME: when testing env switches to puppeteer
    if (process.env.NODE_ENV === 'test') {
      const index = focusableElement.findIndex(
        (element: Element) => element === e.target
      )
      if (index !== -1) {
        focusableElement[goingBackward ? index - 1 : index + 1]?.focus()
      }
    }
  }
}

const TrapFocus: ObjectDirective = {
  beforeMount(el: ITrapFocusElement) {
    el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
    FOCUS_STACK.push(el)
    if (FOCUS_STACK.length <= 1) {
      on(document, 'keydown', FOCUS_HANDLER)
    }
  },
  updated(el: ITrapFocusElement) {
    nextTick(() => {
      el[FOCUSABLE_CHILDREN] = obtainAllFocusableElements(el)
    })
  },
  unmounted() {
    FOCUS_STACK.shift()
    if (FOCUS_STACK.length === 0) {
      off(document, 'keydown', FOCUS_HANDLER)
    }
  },
}

export default TrapFocus
