/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 *
 * @flow
 */

import { getModality } from '../modality/index'
import { useEvent } from '../useEvent/index'
import { useLayoutEffectImpl as useLayoutEffect } from '../useLayoutEffect/index'

/**
 * Types
 */

export type HoverEventsConfig = {
  contain?: boolean | null
  disabled?: boolean | null
  onHoverStart?: ((e: any) => void) | null
  onHoverChange?: ((bool: boolean) => void) | null
  onHoverUpdate?: ((e: any) => void) | null
  onHoverEnd?: ((e: any) => void) | null
}

/**
 * Implementation
 */

const emptyObject = {} as any
const opts = { passive: true }
const lockEventType = 'react-gui:hover:lock'
const unlockEventType = 'react-gui:hover:unlock'
const supportsPointerEvent = () =>
  !!(typeof window !== 'undefined' && window.PointerEvent != null)

function dispatchCustomEvent(
  target: EventTarget,
  type: string,
  payload?: {
    bubbles?: boolean
    cancelable?: boolean
    detail?: {
      [K in string]: unknown
    }
  }
) {
  const event = document.createEvent('CustomEvent')
  const { bubbles = true, cancelable = true, detail } = payload || emptyObject
  event.initCustomEvent(type, bubbles, cancelable, detail)
  target.dispatchEvent(event)
}

// This accounts for the non-PointerEvent fallback events.
function getPointerType(event) {
  const { pointerType } = event
  return pointerType != null ? pointerType : getModality()
}

export function useHover(targetRef: any, config: HoverEventsConfig): void {
  const { contain, disabled, onHoverStart, onHoverChange, onHoverUpdate, onHoverEnd } =
    config

  const canUsePE = supportsPointerEvent()

  const addMoveListener = useEvent(canUsePE ? 'pointermove' : 'mousemove', opts)
  const addEnterListener = useEvent(canUsePE ? 'pointerenter' : 'mouseenter', opts)
  const addLeaveListener = useEvent(canUsePE ? 'pointerleave' : 'mouseleave', opts)
  // These custom events are used to implement the "contain" prop.
  const addLockListener = useEvent(lockEventType, opts)
  const addUnlockListener = useEvent(unlockEventType, opts)

  useLayoutEffect(() => {
    const target = targetRef.current
    if (target !== null) {
      /**
       * End the hover gesture
       */
      const hoverEnd = function (e) {
        if (onHoverEnd != null) {
          onHoverEnd(e)
        }
        if (onHoverChange != null) {
          onHoverChange(false)
        }
        // Remove the listeners once finished.
        addMoveListener(target, null)
        addLeaveListener(target, null)
      }

      /**
       * Leave element
       */
      const leaveListener = function (e) {
        const target = targetRef.current
        if (target != null && getPointerType(e) !== 'touch') {
          if (contain) {
            dispatchCustomEvent(target, unlockEventType)
          }
          hoverEnd(e)
        }
      }

      /**
       * Move within element
       */
      const moveListener = function (e) {
        if (getPointerType(e) !== 'touch') {
          if (onHoverUpdate != null) {
            // Not all browsers have these properties
            if (e.x == null) {
              e.x = e.clientX
            }
            if (e.y == null) {
              e.y = e.clientY
            }
            onHoverUpdate(e)
          }
        }
      }

      /**
       * Start the hover gesture
       */
      const hoverStart = function (e) {
        if (onHoverStart != null) {
          onHoverStart(e)
        }
        if (onHoverChange != null) {
          onHoverChange(true)
        }
        // Set the listeners needed for the rest of the hover gesture.
        if (onHoverUpdate != null) {
          addMoveListener(target, !disabled ? moveListener : null)
        }
        addLeaveListener(target, !disabled ? leaveListener : null)
      }

      /**
       * Enter element
       */
      const enterListener = function (e) {
        const target = targetRef.current
        if (target != null && getPointerType(e) !== 'touch') {
          if (contain) {
            dispatchCustomEvent(target, lockEventType)
          }
          hoverStart(e)
          const lockListener = function (lockEvent) {
            if (lockEvent.target !== target) {
              hoverEnd(e)
            }
          }
          const unlockListener = function (lockEvent) {
            if (lockEvent.target !== target) {
              hoverStart(e)
            }
          }
          addLockListener(target, !disabled ? lockListener : null)
          addUnlockListener(target, !disabled ? unlockListener : null)
        }
      }

      addEnterListener(target, !disabled ? enterListener : null)
    }
  }, [
    addEnterListener,
    addMoveListener,
    addLeaveListener,
    addLockListener,
    addUnlockListener,
    contain,
    disabled,
    onHoverStart,
    onHoverChange,
    onHoverUpdate,
    onHoverEnd,
    targetRef,
  ])
}
