import { createFocusTrap, FocusTrap } from 'focus-trap'
import { render, toChildArray, RefObject } from 'preact'
import { FC } from 'preact/compat'
import { useEffect, useLayoutEffect, useRef, useState } from 'preact/hooks'
import { useGeneratedId } from 'ui/hooks/seamly-hooks'
import { childIsVNode, createAriaHider } from 'ui/utils/general-utils'

type ModalProps = {
  onClose: Function
  fallBackFocusRef: RefObject<HTMLDivElement>
  'aria-label'?: string
  'aria-labelledby'?: string
}

const Modal: FC<ModalProps> = ({
  children,
  onClose,
  fallBackFocusRef,
  'aria-label': ariaLabel,
  'aria-labelledby': ariaLabelledBy,
}) => {
  const modalId = useGeneratedId()
  const modalContainer = useRef<HTMLDivElement | null>(null)
  const focusTrap = useRef<FocusTrap | null>(null)
  const [containerIsSet, setContainerIsSet] = useState(false)

  useEffect(() => {
    if (containerIsSet && modalContainer.current) {
      focusTrap.current = createFocusTrap(modalContainer.current, {
        initialFocus: fallBackFocusRef.current || undefined,
      })
      focusTrap.current.activate()
    }
    return () => {
      if (focusTrap.current) {
        focusTrap.current.deactivate()
      }
    }
  }, [containerIsSet, fallBackFocusRef])

  useEffect(() => {
    const disposeAriaHider = createAriaHider()
    return () => {
      disposeAriaHider()
    }
  }, [containerIsSet])

  useEffect(() => {
    if (containerIsSet && modalContainer.current) {
      modalContainer.current.addEventListener('keydown', (event) => {
        if ((event.code && event.code === 'Escape') || event.keyCode === 27) {
          onClose()
        }
      })
    }
  }, [containerIsSet, onClose])

  useLayoutEffect(() => {
    const bodyElement = document.getElementsByTagName('body')[0]
    const container = document.createElement('div')
    container.setAttribute('id', modalId)
    container.setAttribute('role', 'dialog')
    container.setAttribute('data-nosnippet', 'true')
    container.setAttribute('aria-modal', 'true')
    if (ariaLabel) {
      container.setAttribute('aria-label', ariaLabel)
    }
    if (ariaLabelledBy) {
      container.setAttribute('aria-labelledby', ariaLabelledBy)
    }
    bodyElement.appendChild(container)
    modalContainer.current = container
    setContainerIsSet(true)
    return () => {
      if (modalContainer.current) {
        bodyElement.removeChild(modalContainer.current)
        modalContainer.current = null
      }
    }
  }, [ariaLabel, ariaLabelledBy, modalId])

  // This component can either be provided with a children render
  // function or another component.
  // If a render function the function will be called with the onClose
  // handler function as well as a modalRenderFn that should be used to
  // render your content.
  //   <Modal onClose={onCloseHandler} aria-label="label test">
  //    {({ onClose, modalRenderFn }) =>
  //        modalRenderFn(<MyContent onClose={onClose} />)
  //    }
  //   </Modal>
  // If called with a single component, no render function is required
  // and the onClose function will be automatically added to the
  // single child component.
  //   <Modal onClose={onCloseHandler} aria-label="label test" >
  //       <MyContent />
  //   </Modal>
  return typeof children === 'function'
    ? children({
        onClose,
        modalRenderFn: (els) =>
          modalContainer.current && render(els, modalContainer.current),
      })
    : modalContainer.current &&
        render(
          toChildArray(children)
            .filter(childIsVNode)
            .map((child) => {
              child.props = { ...child.props, onClose }
              return child
            }),
          modalContainer.current,
        )
}

export default Modal
