import Tether from '../main/webapp/v4/dom/Tether'
import { v1 as uuidv1 } from 'uuid'

/*
1st of 2 functions to call in order to make a dropdown menu ARIA-accessible. See docs/accessibility-best-practices.md for further instructions.

Programmatically sets some required ARIA attributes, such as "role" and "aria-controls",
and sets up listeners for keyboard activity.

Parameters:
  # list: <ul> or <div> with `role: menu`
  # button: opens/closes the popup menu
*/
export function setupMenuForAria(list, button) {
  formatMenuItems(list)
  setAriaControls(list, button)
  listenForFocusChanges(list, button)
}

/*
2nd of 2 functions to call in order to make a dropdown menu ARIA-accessible. See docs/accessibility-best-practices.md for further instructions.

Opens/closes menu, setting metadata appropriately. Places focus on first element and sets up
a listener to automatically close menu when focus moves away.

Parameters:
  # list: <ul> or <div> with `role: menu`
  # button: opens/closes the popup menu
  # (optional) target: element to receive `is-open` class; defaults to `list` element
*/
export function toggleMenu(list, button, target = list) {
  const collectionSelector = list.querySelector('.ComboInput') // temporary solution to ensure user can tab to Collection ComboInput in SearchResultActions
  const triggerCloseMenu = (event) => {
    if (
      button.classList.contains('is-active') &&
      event.key === 'Tab' &&
      !list.contains(document.activeElement) &&
      document.activeElement !== collectionSelector &&
      document.activeElement !== button
    ) {
      button.click()
    }
  }

  resetTabIndices(list, '[role="menuitem"]')
  Tether.toggle(button, list, () => {
    const isActive = button.classList.toggle('is-active')
    if (isActive) {
      button.setAttribute('aria-expanded', true)
      window.addEventListener('keyup', triggerCloseMenu)
      list.addEventListener(
        'transitionend',
        () => {
          const visibleItems = findVisibleItems(list)
          if (!visibleItems?.length) return
          const firstFocus = getFirstFocus(visibleItems)
          firstFocus?.setAttribute('tabindex', '0')
          firstFocus?.focus()
        },
        { once: true },
      )
    } else {
      button.setAttribute('aria-expanded', false)
      window.removeEventListener('keyup', triggerCloseMenu)
    }
    return target.classList.toggle('is-open', isActive)
  })
}

/*
Find menu items and set tab indices to -1.

PREFERRED USAGE: Menu options already setup with role: "menuitem"
ALTERNATIVE USAGE: Defaults to applying role: "menuitem" to any visible buttons & anchor elements within the menu
*/
function formatMenuItems(list) {
  resetTabIndices(list, 'button, a')

  // failsafe to catch alternative usage
  const menuItemExists = list.querySelector('[role="menuitem"]')
  if (!menuItemExists) {
    const buttons = list.querySelectorAll('button, a')
    buttons.forEach((button) => {
      if (!button.hasAttribute('role')) {
        button.setAttribute('role', 'menuitem')
        button.classList.add('Dropdown-item')
      }
    })
  }
}

/*
Programmatically associates menu toggle with the menu it controls
*/
function setAriaControls(list, button) {
  if (!button.hasAttribute('aria-controls')) {
    if (!list.id) list.id = uuidv1()
    button.setAttribute('aria-controls', list.id)
  }
}

function listenForFocusChanges(list, button) {
  listenForArrowKeyOnMenuToggle(list, button)
  listenForArrowKeyInMenu(list, button)
  listenForMenuItemClick(list, button)
}

/*
Closes the menu whenever an interior button or link, regardless of its role, is clicked
*/
export function listenForMenuItemClick(list, button) {
  const listitems = list.querySelectorAll('button, a, [role="menuitem"]')
  listitems.forEach((item) => {
    item.addEventListener('click', () => {
      if (button.getAttribute('aria-expanded') === 'true') {
        button.click()
        button.focus()
      }
    })
  })
}

/*
Allows the following interactions:
  1. User presses Right or Down Arrow Key on menu toggle to open menu and place focus on first menu item
  2. User presses Left or Up Arrow Key on menu toggle to open menu and place focus on last menu item

Exceptions:
  If menu toggle is within a toolbar the orientation of the toolbar will determine
  which arrow keys will open the menu and move focus.
  - Horizontal toolbars (default) will only allow the up/dowm arrow keys to open the menu.
  - Vertical toolbars will only allow the left/right arrow keys to open the menu.
*/
function listenForArrowKeyOnMenuToggle(list, button) {
  button.addEventListener('keydown', (e) => {
    const tb = list.closest('[role="toolbar"]')
    const tbOrientation = tb?.getAttribute('aria-orientation')

    switch (e.key) {
      case 'ArrowDown':
      case 'ArrowRight':
        // Exit early.
        if (
          !!tb &&
          ((tbOrientation === 'vertical' && e.key === 'ArrowDown') ||
            (tbOrientation !== 'vertical' && e.key === 'ArrowRight'))
        ) {
          break
        }
        button.click()
        e.stopPropagation()
        e.preventDefault()
        break
      case 'ArrowUp':
      case 'ArrowLeft':
        // Exit early.
        if (
          !!tb &&
          ((tbOrientation === 'vertical' && e.key === 'ArrowUp') ||
            (tbOrientation !== 'vertical' && e.key === 'ArrowLeft'))
        ) {
          break
        }
        button.click()
        if (button.getAttribute('aria-expanded') === 'true') {
          list.addEventListener(
            'transitionend',
            () => {
              changeFocus(getFirstFocus(findVisibleItems(list)), -1)
            },
            { once: true },
          )
        }
        break
    }
  })
}

/*
Allows user to use arrow keys to navigate the menu
*/
function listenForArrowKeyInMenu(list, button) {
  list.addEventListener('keydown', (e) => {
    switch (e.key) {
      case 'Escape':
        button.click()
        button.focus()
        e.preventDefault()
        e.stopPropagation()
        break
      case 'ArrowLeft':
      case 'ArrowUp':
        changeFocus(e.target, -1)
        e.preventDefault()
        break
      case 'ArrowRight':
      case 'ArrowDown':
        changeFocus(e.target, 1)
        e.preventDefault()
        break
    }
  })
}

function changeFocus(currentFocus, move) {
  const list = findList(currentFocus)
  const menuitems = findVisibleItems(list)
  if (!list || !menuitems) return

  const curIndex = menuitems.indexOf(currentFocus)
  if (curIndex === -1) return
  currentFocus.setAttribute('tabindex', '-1')

  // If a focus classname was used, remove it.
  const itemWithFocusClass = list.querySelector('li.focus')
  if (itemWithFocusClass) {
    itemWithFocusClass.classList.remove('focus')
  }

  // move focus to previous item or wrap around to end
  if (move === -1) {
    let targetElement =
      curIndex - 1 > -1
        ? menuitems[curIndex - 1]
        : menuitems[menuitems.length - 1]
    targetElement.setAttribute('tabindex', '0')
    targetElement.focus()
    // If a focus classname was used, restore it.
    if (itemWithFocusClass) {
      targetElement.closest('li')?.classList.add('focus')
    }
  }
  // move focus to next item or wrap around to beginning
  if (move === 1) {
    let targetElement =
      curIndex + 1 < menuitems.length ? menuitems[curIndex + 1] : menuitems[0]
    targetElement.setAttribute('tabindex', '0')
    targetElement.focus()
    // If a focus classname was used, restore it.
    if (itemWithFocusClass) {
      targetElement.closest('li')?.classList.add('focus')
    }
  }
}

function resetTabIndices(list, query) {
  const itemsToReset = list.querySelectorAll(query)
  itemsToReset.forEach((item) => {
    item.setAttribute('tabindex', '-1')
  })
}

function findVisibleItems(list) {
  if (!list) return
  return Array.from(list.querySelectorAll('[role="menuitem"]')).filter(
    (item) => {
      return (
        !item.hasAttribute('aria-hidden') &&
        !item.closest('[aria-hidden="true"]') &&
        !(window.getComputedStyle(item).display === 'none') &&
        !item.classList.contains('screen-reader-only')
      )
    },
  )
}

function getFirstFocus(menuitems) {
  for (const item of menuitems) {
    if (
      item.matches('.is-selected, .selected') ||
      item.parentElement.matches('.is-selected, .selected')
    ) {
      return item
    }
  }
  return menuitems[0]
}

function findList(item) {
  if (!item) return
  return item.closest('.is-open') || item.closest('ul')
}
