import { React, SimpleBar, classnames, PropTypes } from '@gravityforms/libraries';
import { getClosest } from '@gravityforms/utils';
import {
	useId,
	useStateWithDep,
	ConditionalWrapper,
	useFocusTrap,
} from '@gravityforms/react-utils';
import Button from '../../elements/Button';
import Heading from '../../elements/Heading';
import Text from '../../elements/Text';

const { forwardRef, useState, useRef, useEffect } = React;

/**
 * @module Flyout
 * @description A flyout component in react.
 *
 * @since 1.1.18
 *
 * @param {object}              props                                   Component props.
 * @param {JSX.Element}         props.afterContent                      Any custom content to be placed after the body of the flyout.
 * @param {number}              props.animationDelay                    Total runtime of close animation. Synchronize with css if modifying the built-in 250 ms delay.
 * @param {JSX.Element}         props.beforeContent                     Any custom content to be placed before the header of the flyout.
 * @param {JSX.Element}         props.children                          React element children in the flyout body.
 * @param {object}              props.closeButtonCustomAttributes       Custom attributes for the close button.
 * @param {boolean}             props.closeOnMaskClick                  Whether to close if the background mask is clicked.
 * @param {object}              props.customAttributes                  Custom attributes for the component.
 * @param {string|Array|object} props.customBodyClasses                 Custom classes for the flyout body as array.
 * @param {string|Array|object} props.customClasses                     Custom classes for the component.
 * @param {string|Array|object} props.customInnerBodyClasses            Custom classes for the flyout inner_body as array.
 * @param {string|Array|object} props.customMaskClasses                 Custom classes for the mask as array.
 * @param {string}              props.description                       Subheading description for the flyout.
 * @param {string}              props.desktopWidth                      Width for the flyout on desktop.
 * @param {string}              props.direction                         Flyout direction, left or right.
 * @param {JSX.Element}         props.headerChildrenLeft                React element children in the flyout header on the left.
 * @param {JSX.Element}         props.headerChildrenRight               React element children in the flyout header on the right.
 * @param {object}              props.headerDescriptionCustomAttributes Custom attributes for the header description.
 * @param {object}              props.headerHeadingCustomAttributes     Custom attributes for the header heading.
 * @param {boolean}             props.headerIsFixed                     Whether or not the header is fixed.
 * @param {string}              props.id                                Flyout id.
 * @param {boolean}             props.isOpen                            Prop to control whether the dialog is currently open.
 * @param {boolean}             props.maskBlur                          Whether to blur behind the mask for the flyout.
 * @param {string}              props.maskTheme                         Mask background color scheme: `none`, `light` or `dark`
 * @param {number}              props.maxWidth                          Max width in pixels for the flyout.
 * @param {number}              props.mobileBreakpoint                  Mobile breakpoint in pixels for the flyout.
 * @param {string}              props.mobileWidth                       Width for the flyout on mobile.
 * @param {Function}            props.onClose                           Function to fire on flyout close.
 * @param {Function}            props.onOpen                            Function to fire on flyout open.
 * @param {string}              props.position                          The position of the flyout, `absolute` or `fixed`.
 * @param {boolean}             props.showDivider                       Whether or not to show the divider border below title.
 * @param {boolean}             props.simplebar                         Whether or not to use SimpleBar on the content.
 * @param {string}              props.title                             The title of the flyout.
 * @param {number}              props.zIndex                            z-index of the flyout.
 * @param {object|null}         ref                                     Ref to the component.
 *
 * @return {JSX.Element} The Flyout component.
 *
 * @example
 * import Flyout from '@gravityforms/components/react/admin/modules/Flyout';
 *
 * return (
 *     <Flyout direction="right" title="Flyout title">
 *         { children }
 *     </Flyout>
 * );
 *
 */
const Flyout = forwardRef( ( {
	afterContent = null,
	animationDelay = 250,
	beforeContent = null,
	children = null,
	closeButtonCustomAttributes = {
		customClasses: [],
		icon: 'delete',
		iconPrefix: 'gravity-component-icon',
		size: 'size-xs',
		title: '',
		type: 'round',
	},
	closeOnMaskClick = true,
	customAttributes = {},
	customBodyClasses = [],
	customClasses = [],
	customInnerBodyClasses = [],
	customMaskClasses = [],
	description = '',
	desktopWidth = '0',
	direction = '',
	headerChildrenLeft = null,
	headerChildrenRight = null,
	headerDescriptionCustomAttributes = {},
	headerHeadingCustomAttributes = {},
	headerIsFixed = false,
	id: defaultId = '',
	isOpen = false,
	maskBlur = false,
	maskTheme = 'none',
	maxWidth = 0,
	mobileBreakpoint = 0,
	mobileWidth = '100%',
	onClose = () => {},
	onOpen = () => {},
	position = 'fixed',
	showDivider = true,
	simplebar = false,
	title = '',
	zIndex = 10,
}, ref ) => {  // eslint-disable-line
	const [ animationReady, setAnimationReady ] = useState( false );
	const [ animationActive, setAnimationActive ] = useState( false );
	const [ flyoutActive, setFlyoutActive ] = useStateWithDep( isOpen );
	const trapRef = useFocusTrap( flyoutActive );
	const closeRef = useRef( true );
	const id = useId( defaultId );

	useEffect( () => {
		if ( flyoutActive ) {
			showFlyout();
		} else if ( ! flyoutActive ) {
			closeFlyout();
		}
	}, [ flyoutActive ] );

	useEffect( () => {
		closeRef.current.addEventListener( 'keydown', handleEscapeRequest );

		return () => {
			if ( ! closeRef.current ) {
				return;
			}
			closeRef.current.removeEventListener( 'keydown', handleEscapeRequest );
		};
	} );

	const handleEscapeRequest = ( e ) => {
		if ( getClosest( e.target, '.gform-flyout' ) !== closeRef.current ) {
			return;
		}

		if ( e.key !== 'Escape' ) {
			return;
		}

		closeFlyout();
	};

	const pointerDownOrigin = useRef( null );

	const handlePointerDown = ( event ) => {
		pointerDownOrigin.current = event.target;
	};

	const handlePointerUp = ( event ) => {
		if ( pointerDownOrigin.current === event.target &&
			event.target.classList.contains( 'gform-flyout__mask' ) &&
			closeOnMaskClick &&
			flyoutActive ) {
			event.stopPropagation();
			setFlyoutActive( false );
		}
		pointerDownOrigin.current = null;
	};

	const maskProps = {
		className: classnames( {
			'gform-flyout__mask': true,
			'gform-flyout--anim-in-ready': animationReady,
			'gform-flyout--anim-in-active': animationReady && animationActive,
			[ `gform-flyout__mask--position-${ position }` ]: true,
			[ `gform-flyout__mask--theme-${ maskTheme }` ]: true,
			'gform-flyout__mask--blur': maskBlur,
		}, customMaskClasses ),
		onPointerDown: handlePointerDown,
		onPointerUp: handlePointerUp,
		style: {
			zIndex,
		},
	};

	const componentProps = {
		className: classnames( {
			'gform-flyout': true,
			'gform-flyout--anim-in-ready': animationReady,
			'gform-flyout--anim-in-active': animationReady && animationActive,
			[ `gform-flyout--${ direction }` ]: true,
			[ `gform-flyout--${ position }` ]: true,
			'gform-flyout--divider': showDivider,
			'gform-flyout--no-divider': ! showDivider,
			'gform-flyout--no-description': ! description,
			'gform-flyout--simplebar': simplebar,
			'gform-flyout--header-fixed': headerIsFixed,
		}, customClasses ),
		id,
		...customAttributes,
	};

	/**
	 * @function showFlyout
	 * @description Opens the flyout and fires the `onOpen` function if passed in.
	 *
	 * @since 1.1.18
	 *
	 * @return {void}
	 */
	const showFlyout = () => {
		setAnimationReady( true );
		setTimeout( () => {
			setAnimationActive( true );
			onOpen();
		}, 25 );
	};

	/**
	 * @function closeFlyout
	 * @description Closes the flyout and fires the `onClose` function if passed in.
	 *
	 * @since 1.1.18
	 *
	 * @return {void}
	 */
	const closeFlyout = () => {
		setAnimationActive( false );

		setTimeout( () => {
			setAnimationReady( false );
			onClose();
		}, animationDelay );
	};

	/**
	 * @function getHeader
	 * @description Returns the header of the flyout that contains the title, description, children, and close button.
	 *
	 * @since 1.1.18
	 *
	 * @return {JSX.Element}
	 */
	const getHeader = () => {
		let buttonType = 'unstyled';
		if ( closeButtonCustomAttributes.type === 'round' ) {
			buttonType = 'white';
		} else if ( closeButtonCustomAttributes.type === 'simplified' ) {
			buttonType = 'simplified';
		}

		const closeButtonProps = {
			customClasses: classnames(
				{
					'gform-button': true,
					'gform-flyout__close': true,
				},
				closeButtonCustomAttributes.customClasses || [],
			),
			circular: closeButtonCustomAttributes.type === 'round',
			onClick: () => setFlyoutActive( false ),
			size: 'size-height-s',
			type: buttonType,
			...closeButtonCustomAttributes,
		};

		const headerProps = {
			className: classnames( {
				'gform-flyout__head': true,
			} ),
		};

		const headingProps = {
			customClasses: classnames( {
				'gform-flyout__title': true,
			} ),
			content: title,
			size: 'display-xs',
			tagName: 'h3',
			weight: 'SemiBold',
			...headerHeadingCustomAttributes,
		};

		const descriptionProps = {
			customClasses: classnames( {
				'gform-flyout__desc': true,
			} ),
			content: description,
			...headerDescriptionCustomAttributes,
		};

		return (
			<header { ...headerProps }>
				<div className="gform-flyout__head-left">
					{ title && <Heading { ...headingProps } /> }
					{ description && <Text { ...descriptionProps } /> }
					{ headerChildrenLeft }
				</div>
				<div className="gform-flyout__head-right">
					{ headerChildrenRight }
					{ <Button { ...closeButtonProps } /> }
				</div>
			</header>
		);
	};

	/**
	 * @function getBody
	 * @description Returns the body that wrap the children of the flyout.
	 *
	 * @since 1.1.18
	 *
	 * @return {JSX.Element}
	 */
	const getBody = () => {
		const bodyProps = {
			className: classnames( {
				'gform-flyout__body': true,
			}, customBodyClasses ),
		};

		const innerBodyProps = {
			className: classnames( {
				'gform-flyout__body-inner': true,
			}, customInnerBodyClasses ),
		};

		return (
			<div { ...bodyProps } >
				<div { ...innerBodyProps } >
					{ children }
				</div>
			</div>
		);
	};

	/**
	 * @function getCSS
	 * @description Returns the CSS used to handle the width and mobile media query.
	 *
	 * @since 1.1.18
	 *
	 * @return {string} The CSS for the flyout.
	 */
	const getCSS = () => {
		let css = `#${ id } {
			max-width: ${ maxWidth ? `${ maxWidth }px` : 'none' };
			width: ${ mobileWidth };
			z-index: ${ zIndex }
		}
		`;

		if ( mobileBreakpoint ) {
			css += `
				@media only screen and (min-width: ${ mobileBreakpoint }px) {
					#${ id } {
						width: ${ desktopWidth };
					}
				}
			`;
		}

		return css;
	};

	return (
		<div { ...maskProps } ref={ trapRef }>
			<article { ...componentProps } ref={ closeRef }>
				<ConditionalWrapper
					condition={ simplebar }
					wrapper={ ( ch ) => <SimpleBar>{ ch }</SimpleBar> }
				>
					{ beforeContent }
					{ getHeader() }
					{ getBody() }
					{ afterContent }
				</ConditionalWrapper>
			</article>
			<style>
				{ getCSS() }
			</style>
		</div>
	);
} );

Flyout.propTypes = {
	afterContent: PropTypes.node,
	animationDelay: PropTypes.number,
	beforeContent: PropTypes.node,
	children: PropTypes.oneOfType( [
		PropTypes.arrayOf( PropTypes.node ),
		PropTypes.node,
	] ),
	closeButtonCustomAttributes: PropTypes.object,
	closeOnMaskClick: PropTypes.bool,
	customAttributes: PropTypes.object,
	customBodyClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	customInnerBodyClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	description: PropTypes.string,
	desktopWidth: PropTypes.string,
	direction: PropTypes.string,
	headerChildrenLeft: PropTypes.oneOfType( [
		PropTypes.arrayOf( PropTypes.node ),
		PropTypes.node,
	] ),
	headerChildrenRight: PropTypes.oneOfType( [
		PropTypes.arrayOf( PropTypes.node ),
		PropTypes.node,
	] ),
	headerDescriptionCustomAttributes: PropTypes.object,
	headerHeadingCustomAttributes: PropTypes.object,
	headerIsFixed: PropTypes.bool,
	id: PropTypes.string,
	isOpen: PropTypes.bool,
	maskBlur: PropTypes.bool,
	maskTheme: PropTypes.string,
	maxWidth: PropTypes.number,
	mobileBreakpoint: PropTypes.number,
	mobileWidth: PropTypes.string,
	onClose: PropTypes.func,
	onOpen: PropTypes.func,
	position: PropTypes.oneOf( [ 'absolute', 'fixed' ] ),
	showDivider: PropTypes.bool,
	simplebar: PropTypes.bool,
	title: PropTypes.string,
	zIndex: PropTypes.number,
};

Flyout.displayName = 'Flyout';

export default Flyout;
