import { defaultTransitionTimeMs } from 'config'
import { className } from 'lib/css'
import { cloneElement, toChildArray } from 'preact'
import { FC } from 'preact/compat'
import { useEffect, useMemo, useRef, useState } from 'preact/hooks'
import useTimeout from 'ui/hooks/use-timeout'
import { ValueOf, childIsVNode } from 'ui/utils/general-utils'

const transitionClasses = {
  visible: className('transition--visible'),
  in: className('transition--in'),
  visuallyHidden: className('visually-hidden'),
}

const transitionClassesArray = Object.values(transitionClasses)

export const useStableCallback = (callback) => {
  const callbackRef = useRef(null)
  callbackRef.current = callback
  const isFunction = typeof callback === 'function'
  return useMemo(() => {
    // @ts-ignore
    return isFunction ? (...args) => callbackRef.current(...args) : undefined
  }, [isFunction])
}

export const transitionStartStates = {
  notRendered: 'notRendered',
  rendered: 'rendered',
  visuallyHidden: 'visuallyHidden',
} as const

type InOutTransitionProps = {
  isActive: boolean
  timeout?: number
  transitionStartState?: keyof typeof transitionStartStates
  onInTransitionComplete?: Function
  onOutTransitionComplete?: Function
  exitAfter?: number
  enterDelay?: number
}

const InOutTransition: FC<InOutTransitionProps> = ({
  children,
  isActive,
  timeout = defaultTransitionTimeMs,
  transitionStartState = transitionStartStates.notRendered,
  onInTransitionComplete = () => {},
  onOutTransitionComplete = () => {},
  exitAfter,
  enterDelay = 0,
}) => {
  const prevIsActive = useRef(false)
  const activeTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(null)
  const activeRafRef = useRef<ReturnType<typeof requestAnimationFrame> | null>(
    null,
  )

  const isVisuallyHidden =
    transitionStartState === transitionStartStates.visuallyHidden

  const [activeTransitionClasses, setActiveTransitionClasses] = useState<
    ValueOf<typeof transitionClasses>[]
  >(() => (isVisuallyHidden ? [transitionClasses.visuallyHidden] : []))

  const [inState, setInState] = useState(() => enterDelay === 0)

  const transitionOutAfter = useMemo(
    () => (exitAfter ? exitAfter + enterDelay : exitAfter),
    [enterDelay, exitAfter],
  )

  useTimeout(() => setInState(false), isActive ? transitionOutAfter : undefined)
  useTimeout(() => setInState(true), isActive ? enterDelay : undefined)

  const transitionIn = useMemo(() => inState && isActive, [isActive, inState])

  const onInTransitionCompleteHandler = useStableCallback(
    onInTransitionComplete,
  )

  const onOutTransitionCompleteHandler = useStableCallback(
    onOutTransitionComplete,
  )

  const renderChildren =
    transitionStartState !== 'notRendered' || activeTransitionClasses.length > 0

  useEffect(() => {
    if (prevIsActive.current && !transitionIn) {
      setActiveTransitionClasses([transitionClasses.visible])
      activeTimeoutRef.current = setTimeout(() => {
        setActiveTransitionClasses([
          ...(isVisuallyHidden ? [transitionClasses.visuallyHidden] : []),
        ])

        if (onOutTransitionCompleteHandler) {
          activeRafRef.current = requestAnimationFrame(() => {
            onOutTransitionCompleteHandler()
          })
        }
      }, timeout)
    }

    if (!prevIsActive.current && transitionIn) {
      setActiveTransitionClasses([transitionClasses.visible])
      // Doubling up on rAF as a single rAF can be too slow for the
      // animation transition to be resolved.
      activeRafRef.current = requestAnimationFrame(() => {
        activeRafRef.current = requestAnimationFrame(() => {
          setActiveTransitionClasses([
            transitionClasses.visible,
            transitionClasses.in,
          ])
          if (onInTransitionCompleteHandler) {
            activeTimeoutRef.current = setTimeout(() => {
              onInTransitionCompleteHandler()
            }, timeout)
          }
        })
      })
    }

    prevIsActive.current = transitionIn

    return () => {
      if (activeTimeoutRef.current) clearTimeout(activeTimeoutRef.current)
      if (activeRafRef.current) cancelAnimationFrame(activeRafRef.current)
    }
  }, [
    isVisuallyHidden,
    onInTransitionCompleteHandler,
    onOutTransitionCompleteHandler,
    timeout,
    transitionIn,
  ])

  return (
    <>
      {renderChildren &&
        toChildArray(children)
          .filter(childIsVNode)
          .map((child) => {
            const { className: childClassName = '' } = child.props
            const cleanClasses = childClassName
              .split(' ')
              .filter((cl) => !transitionClassesArray.includes(cl))

            return cloneElement(child, {
              className: [...cleanClasses, ...activeTransitionClasses].join(
                ' ',
              ),
            })
          })}
    </>
  )
}

export default InOutTransition
