/* global ResizeObserver, getComputedStyle */
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
import { Icon } from '../atoms/icon.js'
import { Menu } from '../atoms/menu.js'
import { NavLink } from '../atoms/nav-link.js'
import { ValidationError } from '../routing/error-handler.js'
import getId from '../shared/get-id.js'
import h from '../shared/h.js'
import { ARROW_BACK, ARROW_NEXT } from '../shared/icons.js'
import t from '../shared/t.js'
import { MEDIUM } from '../shared/twind.js'

const SCROLL_TOLERANCE = 2
const MOBILE_STYLE = 'overflow-x: auto; overflow-y: hidden; flex-wrap: nowrap'

export function Tabs ({ _model, _variant, _color, children, ...props }) {
  // We cannot exit here on falsy model to avoid breaking rules of hooks.
  const rawLinks = _model?.links || []
  if (rawLinks.length === 0) {
    throw new ValidationError()
  }

  const hasPanels = Boolean(children?.length)

  // Generate ids for tabs and panels.
  const ids = useMemo(() => ({
    tabs: rawLinks.map(getId), // Needed for the compact variant regardless of panel presence.
    panels: hasPanels && rawLinks.map(getId)
  }), [])
  // Configure links.
  const tabPanelHashes = useMemo(() => hasPanels && rawLinks.map(link =>
    link.name ? `tab-${hashFromString(link.name)}` : getId()), []
  )
  const links = useMemo(() => hasPanels
    ? rawLinks.map((link, i) => ({ ...link, value: `#${tabPanelHashes[i]}` }))
    : rawLinks)

  const [selectedIndex] = useSelectedTab({ rawLinks, tabPanelHashes })

  return h('div', { ...props, className: 'w-full relative', style: '--actual-bg-color: transparent' },
    _variant === 'compact'
      ? h(TabDropdown, { links, hasPanels, ids, selectedIndex })
      : h(TabButtons, { links, hasPanels, ids, selectedIndex }),
    hasPanels && h(TabPanels, { panels: children, ids, selectedIndex })
  )
}

function getInheritedBgColor (element) {
  const defaultColor = 'rgba(0, 0, 0, 0)'
  if (element) {
    const bgColor = getComputedStyle(element).backgroundColor
    if (bgColor !== defaultColor) {
      return bgColor
    }

    // Needs to work in Shadow DOM.
    return getInheritedBgColor(element?.parentElement ?? element?.parentNode?.host)
  }

  return defaultColor
}

function hashFromString (text) {
  return text?.trim().toLowerCase().replaceAll(/\s+/g, '-')
}

function TabButtons ({ links, hasPanels, ids, selectedIndex }) {
  const ref = useRef(null)
  const [state, setState] = useState({ left: false, right: false })

  // Update left and right arrows based on the scroll.
  const updateBlurs = el => {
    setState({
      left: el.scrollWidth > el.clientWidth && Math.abs(el.scrollLeft) > SCROLL_TOLERANCE,
      right: el.scrollWidth > el.clientWidth && Math.abs(el.scrollLeft) < el.scrollWidth - el.clientWidth - SCROLL_TOLERANCE
    })
  }

  useEffect(() => {
    const outerContainer = ref?.current
    if (!outerContainer) {
      return
    }

    // Tabs do not support palettes.
    // The left and right gradient must therefore use the actual background colour.
    // Since there is no currentBackgroundColour variable (akin to currentColour), we need to get it via ref.
    const bgColor = getInheritedBgColor(outerContainer)
    outerContainer.style.setProperty('--actual-bg-color', bgColor)

    // Update left and right arrows on resize.
    const tabContainer = outerContainer.querySelector('[role="tablist"]')
    const resizeObserver = new ResizeObserver(() => updateBlurs(tabContainer))
    resizeObserver.observe(tabContainer)
  }, [])

  return h('div', {
    className: `relative border-txa/20 border-b ${hasPanels ? 'mb-5' : ''}`,
    ref,
    style: '--actual-bg-color: transparent',
    children: [
      h(Arrow, { right: false, visible: state.left }),
      h('div', { // Tabs
        // Make it scrollable on mobile and touch (and with mobile class for testing).
        _style: id => `
          ${id} { scrollbar-width: none; -ms-overflow-style: none }
          ${id}::-webkit-scrollbar { display: none }
          .mobile ${id} { ${MOBILE_STYLE} }
          @media screen and (max-width: ${MEDIUM}px) and (hover: none) {
            ${id} { ${MOBILE_STYLE} }
          }
        `,
        onScroll: event => updateBlurs(event.target),
        className: 'w-full flex flex-wrap px-6',
        role: 'tablist',
        'aria-owns': ids.tabs?.join?.(' '),
        children: links.map((link, ii) => {
          const selected = ii === selectedIndex
          return h('div', { className: 'shrink-0' },
            h(NavLink, {
              id: ids.tabs?.[ii],
              className: 'cursor-pointer',
              role: 'tab',
              'aria-controls': ids.panels?.[ii],
              'aria-selected': selected,
              _selection: 'bottom',
              _size: 'lg',
              _model: {
                value: link.value,
                name: link.name === 'All' ? t('All') : link.name,
                selected
              }
            })
          )
        })
      }),
      h(Arrow, { right: true, visible: state.right })
    ]
  })
}

function Arrow ({ right, visible }) {
  const icon = { false: ARROW_BACK, true: ARROW_NEXT }[right]
  const style = {
    false: 'left: 0; justify-content: left; background: linear-gradient(90deg, var(--actual-bg-color) 33%, transparent)',
    true: 'right: 0; justify-content: right; background: linear-gradient(-90deg, var(--actual-bg-color) 33%, transparent)'
  }[document.dir === 'rtl' ? !right : right]

  return h('div', {
    className: `absolute top-0 bottom-0 w-12 bg-bga items-center animate-fade ${visible ? 'flex' : 'hidden'}`,
    style,
    children: h(Icon, { _size: 'xs', _rtlFlip: true }, icon)
  })
}

function TabDropdown ({ links, hasPanels, ids, selectedIndex }) {
  const [isExpanded, setIsExpanded] = useState(false)
  const buttonRef = useRef(null)
  const menuId = useMemo(() => getId(), [])
  const currentLink = links[selectedIndex]

  const closeDropdown = () => {
    buttonRef.current?.base?.querySelector?.('a')?.focus()
    setIsExpanded(false)
  }

  let menu
  if (isExpanded) {
    // Show menu.
    menu = h(Menu, {
      id: menuId,
      onClick: closeDropdown,
      children: h('div', {
        className: 'py-1',
        role: 'tablist',
        'aria-owns': ids.tabs?.join?.(' '),
        children: links.map((link, ii) => h(NavLink, {
          id: ids.tabs?.[ii],
          _selection: 'right',
          role: 'tab',
          'aria-controls': ids.panels?.[ii],
          'aria-selected': ii === selectedIndex,
          _model: {
            ...link,
            name: link.name === 'All' ? t('All') : link.name,
            selected: ii === selectedIndex
          }
        }))
      })
    })
  } else {
    menu = h('div', { id: menuId, className: 'hidden' })
  }

  return h('div', {
    className: `border-txa/20 border-b ${hasPanels ? 'mb-5' : ''} flex`,
    onKeyDown: event => { event.key === 'Escape' && closeDropdown() },
    children: h('div', { className: 'relative mx-6' },
      h(NavLink, {
        _selection: 'bottom',
        _size: 'lg',
        _highlighted: isExpanded,
        _expanded: false,
        _model: { ...currentLink, selected: true },
        ref: buttonRef,
        tabindex: '0', // Applies focus outline.
        role: 'button',
        'aria-expanded': isExpanded,
        'aria-controls': menuId,
        onClick: event => {
          event.preventDefault()
          if (isExpanded) {
            closeDropdown()
          } else {
            setIsExpanded(true)
          }
        }
      }),
      menu
    )
  })
}

function TabPanels ({ panels, ids, selectedIndex }) {
  return panels?.map?.((child, ii) => {
    const selected = ii === selectedIndex
    const id = ids.panels?.[ii]
    return (selected || id) && h('div', {
      className: `${selected ? 'animate-fade' : 'hidden'}`,
      ...id && {
        role: 'tabpanel',
        id
      }
    }, selected ? child : undefined)
  })
}

function useSelectedTab ({ rawLinks, tabPanelHashes }) {
  const linksSelectedIndex = rawLinks.findIndex(l => l.selected === true) || 0
  const lastLiksSelectedIndex = useRef(linksSelectedIndex)

  const [selectedIndex, setSelectedIndex] = useState(getSelectedIndexByHash)

  function getSelectedIndexByHash (defaultOnlyIfNoHash = false) {
    const hash = globalThis.decodeURIComponent(globalThis.location.hash.substring(1))
    const anchorIndex = tabPanelHashes?.findIndex?.(id => id === hash)
    if (anchorIndex >= 0) {
      return anchorIndex
    } else {
      return (!defaultOnlyIfNoHash || !hash) ? Math.max(linksSelectedIndex, 0) : null
    }
  }

  useEffect(() => {
    const hashChangeHandler = () => {
      // Prevent resetting current tab if the hash was changed to an unknown value (e.g. anchor or a different Tabs organism).
      const newSelectedIndex = getSelectedIndexByHash(true)
      newSelectedIndex !== null && setSelectedIndex(newSelectedIndex)
    }

    globalThis.addEventListener('hashchange', hashChangeHandler)

    return () => {
      globalThis.removeEventListener('hashchange', hashChangeHandler)
    }
  }, [])

  // React on model selected flag change
  useEffect(() => {
    if ((linksSelectedIndex !== lastLiksSelectedIndex.current) && (linksSelectedIndex >= 0)) {
      setSelectedIndex(linksSelectedIndex)
      lastLiksSelectedIndex.current = linksSelectedIndex
    }
  }, [linksSelectedIndex !== lastLiksSelectedIndex.current])

  return [selectedIndex]
}
