/**
 * External dependencies
 */
import type { RefCallback, SyntheticEvent } from 'react';

/**
 * WordPress dependencies
 */
import { useRef, useEffect, useCallback } from '@wordpress/element';
import { ESCAPE } from '@wordpress/keycodes';

/**
 * Internal dependencies
 */
import useConstrainedTabbing from '../use-constrained-tabbing';
import { useFocusOnMount } from '../use-focus-on-mount';
import useFocusReturn from '../use-focus-return';
import useFocusOutside from '../use-focus-outside';
import useMergeRefs from '../use-merge-refs';

type DialogOptions = {
	/**
	 * Determines focus behavior when the dialog mounts.
	 *
	 * - `"firstElement"` focuses the first tabbable element within.
	 * - `"firstInputElement"` focuses the first value control within.
	 * - `true` focuses the element itself.
	 * - `false` does nothing and _should not be used unless an accessible
	 *    substitute behavior is implemented_.
	 *
	 * @default 'firstElement'
	 */
	focusOnMount?: useFocusOnMount.Mode;
	/**
	 * Determines whether tabbing is constrained to within the popover,
	 * preventing keyboard focus from leaving the popover content without
	 * explicit focus elsewhere, or whether the popover remains part of the
	 * wider tab order.
	 * If no value is passed, it will be derived from `focusOnMount`.
	 *
	 * @see focusOnMount
	 * @default `focusOnMount` !== false
	 */
	constrainTabbing?: boolean;
	onClose?: () => void;
	/**
	 * Use the `onClose` prop instead.
	 *
	 * @deprecated
	 */
	__unstableOnClose?: (
		type: string | undefined,
		event: SyntheticEvent
	) => void;
};

type useDialogReturn = [
	RefCallback< HTMLElement >,
	ReturnType< typeof useFocusOutside > & Pick< HTMLElement, 'tabIndex' >,
];

/**
 * Returns a ref and props to apply to a dialog wrapper to enable the following behaviors:
 *  - constrained tabbing.
 *  - focus on mount.
 *  - return focus on unmount.
 *  - focus outside.
 *
 * @param options Dialog Options.
 */
function useDialog( options: DialogOptions ): useDialogReturn {
	const currentOptions = useRef< DialogOptions >( undefined );
	const { constrainTabbing = options.focusOnMount !== false } = options;
	useEffect( () => {
		currentOptions.current = options;
	}, Object.values( options ) );
	const constrainedTabbingRef = useConstrainedTabbing();
	const focusOnMountRef = useFocusOnMount( options.focusOnMount );
	const focusReturnRef = useFocusReturn();
	const focusOutsideProps = useFocusOutside( ( event ) => {
		// This unstable prop  is here only to manage backward compatibility
		// for the Popover component otherwise, the onClose should be enough.
		if ( currentOptions.current?.__unstableOnClose ) {
			currentOptions.current.__unstableOnClose( 'focus-outside', event );
		} else if ( currentOptions.current?.onClose ) {
			currentOptions.current.onClose();
		}
	} );
	const closeOnEscapeRef = useCallback( ( node: HTMLElement ) => {
		if ( ! node ) {
			return;
		}

		node.addEventListener( 'keydown', ( event: KeyboardEvent ) => {
			// Close on escape.
			if (
				event.keyCode === ESCAPE &&
				! event.defaultPrevented &&
				currentOptions.current?.onClose
			) {
				event.preventDefault();
				event.stopPropagation();
				currentOptions.current.onClose();
			}
		} );
	}, [] );

	return [
		useMergeRefs( [
			constrainTabbing ? constrainedTabbingRef : null,
			options.focusOnMount !== false ? focusReturnRef : null,
			options.focusOnMount !== false ? focusOnMountRef : null,
			closeOnEscapeRef,
		] ),
		{
			...focusOutsideProps,
			tabIndex: -1,
		},
	];
}

export default useDialog;
