import { useCallback, useEffect, useMemo, useState } from 'react'
import tippy, { Instance, Props as TippyProps } from 'tippy.js'

import { useCallbackRef } from '../utils'
import { closeOnEscPlugin, matchWidthModifier } from './consts'
import { UsePopoverProps } from './usePopover.types'

/**
 * Controls the logic of instantiating and managing a tippy instance.
 * Takes multiple configuration props that control closing, placement or behavior.
 * Exposes functions used for ref callbacks on consuming components, programatic open/close methods and properties.
 */
export default function usePopover(props: UsePopoverProps = {}) {
  const {
    closeOnBlur = true,
    closeOnEsc = true,
    defaultIsOpen = false,
    flip = true,
    isOpen: isOpenProp,
    matchWidth,
    offset,
    onClose: onCloseProp,
    onOpen: onOpenProp,
    options,
    placement = 'bottom-start',
    trigger = 'click',
  } = props

  const [isOpen, setIsOpen] = useState(isOpenProp ?? defaultIsOpen)
  const [instance, setInstance] = useState<Instance | null>(null)
  const [reference, setReference] = useState<HTMLElement | null>(null)
  const [popper, setPopper] = useState<HTMLElement | null>(null)

  const close = useCallback(() => instance?.hide(), [instance])
  const onClose = useCallbackRef(onCloseProp)
  const onOpen = useCallbackRef(onOpenProp)
  const open = useCallback(() => instance?.show(), [instance])

  const isControlled = isOpenProp !== undefined

  const tippyOptions: Partial<TippyProps> = useMemo(() => {
    const {
      aria,
      offset: optionsOffset,
      onHidden,
      onHide,
      onShow,
      plugins = [],
      popperOptions = {},
      ...otherOptions
    } = options ?? {}

    return {
      aria,
      arrow: false,
      duration: [200, 150],
      hideOnClick: !isControlled ? closeOnBlur : false,
      interactive: true,
      maxWidth: 'none',
      offset: optionsOffset ?? offset ?? [0, 4],
      onHidden: instance => {
        setIsOpen(false)
        onHidden?.(instance)
      },
      onHide: instance => {
        onClose(instance)
        onHide?.(instance)
      },
      onShow: instance => {
        setIsOpen(true)
        onOpen(instance)
        onShow?.(instance)
      },
      placement,
      plugins: [...(!isControlled && closeOnEsc ? [closeOnEscPlugin] : []), ...plugins],
      popperOptions: {
        ...popperOptions,
        modifiers: [
          {
            name: 'flip',
            enabled: flip,
          },
          { ...matchWidthModifier, enabled: !!matchWidth },
          ...(popperOptions.modifiers ?? []),
        ],
      },
      showOnCreate: isOpenProp ?? defaultIsOpen,
      trigger: !isControlled ? trigger : 'manual',
      ...otherOptions,
    }
  }, [closeOnBlur, closeOnEsc, offset, options, placement, trigger])

  // Create a new Tippy instance
  useEffect(() => {
    if (popper !== null && reference !== null) {
      // Destroy old instance to ensure new options work
      instance?.destroy()

      const newInstance = tippy(reference, {
        ...tippyOptions,
        content: popper,
      })

      setInstance(newInstance)
    }
  }, [popper, tippyOptions, trigger])

  // Handle showing/hiding popup in controlled mode
  useEffect(() => {
    if (isControlled) {
      if (isOpenProp) instance?.show()
      else instance?.hide()
    }
  }, [isOpenProp])

  return {
    close,
    instance,
    isOpen,
    open,
    popper,
    reference,
    setPopper,
    setReference,
  }
}

export type UsePopoverType = ReturnType<typeof usePopover>
