import { React, PropTypes, classnames } from '@gravityforms/libraries';
import {
	IdProvider,
	useIdContext,
	usePhoneInputFormatUtilsContext as usePhoneInputFormatUtils,
} from '@gravityforms/react-utils';
import { spacerClasses } from '@gravityforms/utils';
import exampleNumbers from 'libphonenumber-js/mobile/examples';
import { getExampleNumber } from 'libphonenumber-js';
import { getSelectedItemFromValue } from '../Dropdown/utils';
import CountryDropdown from '../Dropdown/CountryDropdown';
import { getCountriesListItemsFromIsos, sortPreferredCountries } from '../Dropdown/CountryDropdown/utils';
import HelpText from '../../elements/HelpText';
import Input from '../../elements/Input';
import {
	getCaretPosition,
	setCaretPosition,
	getTextAfterEraseSelection,
	getSelection,
	getId,
} from './utils';

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

const INPUT_SELECTOR = '.gform-input';
const DROPDOWN_TRIGGER_SELECTOR = '.gform-dropdown__trigger';
const BORDER_STYLE_DEFAULT = 'default';
const BORDER_STYLE_ERROR = 'error';
const BORDER_STYLE_CORRECT = 'correct';

const PhoneComponent = forwardRef( ( props, ref ) => {
	const {
		countries,
		customAttributes,
		customClasses,
		disabled,
		dropdownAttributes,
		dropdownClasses,
		helpTextAttributes,
		helpTextClasses,
		i18n,
		inputAttributes,
		inputClasses,
		international,
		label,
		labelAttributes,
		labelClasses,
		language,
		onChange,
		preferredCountries,
		required,
		requiredLabelAttributes,
		requiredLabelClasses,
		size,
		spacing,
		usePlaceholder,
		useValidation,
		width,
	} = props;

	// Refs
	const internalRef = useRef( null );
	const wrapperRef = ref || internalRef;
	const dropdownRef = useRef( null );
	const inputRef = useRef( null );
	const phoneInputRef = useRef( null );

	const id = useIdContext();
	const { inputFormatUtils } = usePhoneInputFormatUtils();
	const { getFormattedInputText } = inputFormatUtils;

	const [ country, setCountry ] = useState( () => {
		const countriesListItems = getCountriesListItemsFromIsos(
			countries,
			language,
			i18n,
			{
				showFlag: dropdownAttributes?.showFlag || true,
				showCallingCode: dropdownAttributes?.showCallingCode || true,
			},
		);
		if ( dropdownAttributes?.initialValue ) {
			return getSelectedItemFromValue( dropdownAttributes?.initialValue, countriesListItems, false );
		}
		if ( preferredCountries.length ) {
			const preferred = sortPreferredCountries( countriesListItems, preferredCountries, i18n );
			if ( preferred?.[ 0 ]?.type === 'group' ) {
				return preferred?.[ 0 ]?.items?.[ 0 ] || {};
			}
			return preferred?.[ 0 ];
		}
		if ( countriesListItems?.[ 0 ] ) {
			return countriesListItems?.[ 0 ];
		}
		return {};
	} );
	const [ phoneNumber, setPhoneNumber ] = useState( () => {
		if ( inputAttributes?.value ) {
			const { asYouType, formatted } = getFormattedInputText( inputAttributes?.value, 0, country?.value, international );
			return {
				asYouType,
				chars: asYouType?.getChars() || '',
				value: formatted.text,
				numberValue: asYouType?.getNumberValue() || '',
			};
		}
		return {
			asYouType: null,
			chars: '',
			value: '',
			numberValue: '',
		};
	} );
	const [ borderStyle, setBorderStyle ] = useState( BORDER_STYLE_DEFAULT );

	// Set phoneInputRef.
	useEffect( () => {
		if ( ! inputRef.current ) {
			return;
		}
		if ( phoneInputRef.current === inputRef.current.querySelector( INPUT_SELECTOR ) ) {
			return;
		}
		phoneInputRef.current = inputRef.current.querySelector( INPUT_SELECTOR );
	}, [ inputRef, phoneInputRef ] );

	/**
	 * @function getPlaceholder
	 * @description Get the placeholder text for the input.
	 *
	 * @since 5.5.0
	 *
	 * @return {string|undefined} Placeholder text.
	 */
	const getPlaceholder = () => {
		if ( ! usePlaceholder ) {
			return undefined;
		}

		const exampleNumber = getExampleNumber( country?.value, exampleNumbers );
		if ( ! exampleNumber ) {
			return undefined;
		}

		return international ? exampleNumber.formatInternational() : exampleNumber.formatNational();
	};

	/**
	 * @function validatePhoneNumber
	 * @description Validate the phone number.
	 *
	 * @since 5.5.0
	 *
	 */
	const validatePhoneNumber = () => {
		// If validation is disabled, return early.
		if ( ! useValidation ) {
			if ( borderStyle !== BORDER_STYLE_DEFAULT ) {
				setBorderStyle( BORDER_STYLE_DEFAULT );
			}
			return;
		}

		const { asYouType, numberValue } = phoneNumber;
		const isPossible = asYouType?.isPossible();

		if ( numberValue ) {
			setBorderStyle( isPossible ? BORDER_STYLE_CORRECT : BORDER_STYLE_ERROR );
		} else {
			setBorderStyle( BORDER_STYLE_DEFAULT );
		}
	};

	/**
	 * @function onCountryChange
	 * @description Handle country change.
	 *
	 * @since 5.5.0
	 *
	 * @param {Event}  event The event object.
	 * @param {object} value The selected country object.
	 */
	const onCountryChange = ( event, value ) => {
		setCountry( value );
		const { asYouType, formatted } = getFormattedInputText( phoneNumber.chars, 0, value?.value, international );
		const numberValue = asYouType?.getNumberValue() || '';
		setPhoneNumber( {
			asYouType,
			chars: asYouType?.getChars() || '',
			value: formatted.text,
			numberValue,
		} );

		const changeValue = {
			country: value?.value,
			number: numberValue,
			isValid: asYouType?.isPossible() || false,
		};

		onChange( changeValue, event );

		// Validate phone number after state updates.
		requestAnimationFrame( validatePhoneNumber );
	};

	/**
	 * @function handleInputChange
	 * @description Input change handler.
	 *
	 * @since 5.5.0
	 *
	 * @param {string}           value     The input value.
	 * @param {string|undefined} operation The operation to perform.
	 * @param {Event}            event     The event object.
	 */
	const handleInputChange = ( value, operation, event ) => {
		const phoneInput = phoneInputRef.current;
		const caretPosition = getCaretPosition( phoneInput );
		const { asYouType, formatted } = getFormattedInputText( value, caretPosition, country?.value, international, operation );
		const numberValue = asYouType?.getNumberValue() || '';
		setPhoneNumber( {
			asYouType,
			chars: asYouType?.getChars() || '',
			value: formatted.text,
			numberValue,
		} );

		const changeValue = {
			country: country?.value,
			number: numberValue,
			isValid: asYouType?.isPossible() || false,
		};

		// requestAnimationFrame to set caret after rerender.
		requestAnimationFrame( () => {
			setCaretPosition( phoneInput, formatted.caret );
		} );

		onChange( changeValue, event );
	};

	/**
	 * @function onInputChange
	 * @description Handle input change.
	 *
	 * @since 5.5.0
	 *
	 * @param {string} value The input value.
	 * @param {Event}  event The event object.
	 */
	const onInputChange = ( value, event ) => {
		handleInputChange( value, undefined, event );
	};

	/**
	 * @function onInputKeyDown
	 * @description Handle input key down.
	 *
	 * @since 5.5.0
	 *
	 * @param {Event} event The event object.
	 */
	const onInputKeyDown = ( event ) => {
		if ( inputAttributes?.onKeyDown ) {
			inputAttributes?.onKeyDown?.( event );
		}

		// If `onKeyDown()` handler above has called `event.preventDefault()` then ignore this `keydown` event.
		if ( event.defaultPrevented ) {
			return;
		}

		const phoneInput = phoneInputRef.current;
		if ( phoneInput.hasAttribute( 'readonly' ) ) {
			return;
		}

		const operation = event.key;
		if ( ! [ 'Backspace', 'Delete' ].includes( operation ) ) {
			return;
		}

		// Intercept this operation and perform it manually.
		event.preventDefault();

		const value = phoneInput.value;
		const selection = getSelection( phoneInput );

		// If a selection is made, get the text after erasing selection.
		// Else, perform the (character erasing) operation manually.
		const newValue = selection ? getTextAfterEraseSelection( value, selection ) : value;
		const newOp = selection ? undefined : operation;

		handleInputChange( newValue, newOp, event );
	};

	/**
	 * @function onInputBlur
	 * @description Handle input blur.
	 *
	 * @since 5.5.0
	 *
	 * @param {Event} event The event object.
	 */
	const onInputBlur = ( event ) => {
		if ( inputAttributes?.onBlur ) {
			inputAttributes?.onBlur?.( event );
		}

		// If `onBlur()` handler above has called `event.preventDefault()` then ignore this `blur` event.
		if ( event.defaultPrevented ) {
			return;
		}

		validatePhoneNumber();
	};

	/**
	 * @function onLabelClick
	 * @description Handle label click.
	 *
	 * @since 5.5.0
	 *
	 */
	const onLabelClick = () => {
		const dropdownTrigger = dropdownRef?.current?.querySelector( DROPDOWN_TRIGGER_SELECTOR );
		if ( dropdownTrigger ) {
			dropdownTrigger.focus();
		}
	};

	const helpTextId = getId( id, 'help-text' );
	const labelId = getId( id, 'label' );

	const componentProps = {
		className: classnames( {
			'gform-phone': true,
			'wp-exclude-emoji': true,
			[ `gform-phone--size-${ size }` ]: true,
			'gform-phone--disabled': disabled,
			...spacerClasses( spacing ),
		}, customClasses ),
		id,
		style: {
			width: width ? `${ width }px` : undefined,
		},
		...customAttributes,
	};

	const labelProps = {
		className: classnames( [
			'gform-phone__label',
			'gform-text',
			'gform-text--color-port',
			'gform-typography--size-text-sm',
			'gform-typography--weight-medium',
		], labelClasses ),
		...labelAttributes,
		id: labelId,
		onClick: onLabelClick,
	};

	const requiredLabelProps = {
		size: 'text-sm',
		weight: 'medium',
		...requiredLabelAttributes,
		customClasses: classnames( [ 'gform-phone__required' ], requiredLabelClasses ),
	};

	const dropdownProps = {
		countries,
		hasSearch: true,
		popoverMaxHeight: 300,
		...dropdownAttributes,
		customClasses: classnames( [
			'gform-phone__dropdown',
		], dropdownClasses ),
		disabled,
		i18n,
		id: getId( id, 'dropdown' ),
		language,
		onChange: onCountryChange,
		preferredCountries,
		searchAttributes: {
			wrapperClasses: [ 'gform-phone__dropdown-search-wrapper' ],
			...( dropdownAttributes?.searchAttributes || {} ),
		},
		searchClasses: [ 'gform-phone__dropdown-search' ],
		size,
		triggerAttributes: {
			'aria-labelledby': label ? labelId : undefined,
		},
		triggerClasses: [ 'gform-phone__dropdown-trigger' ],
	};

	const inputProps = {
		...inputAttributes,
		borderStyle,
		customAttributes: {
			'aria-describedby': helpTextAttributes.content && borderStyle === BORDER_STYLE_ERROR ? helpTextId : undefined,
			'aria-labelledby': label ? labelId : undefined,
		},
		customClasses: classnames( [
			'gform-phone__input',
		], inputClasses ),
		directControlled: true,
		disabled,
		id: getId( id, 'input' ),
		onBlur: onInputBlur,
		onChange: onInputChange,
		onKeyDown: onInputKeyDown,
		placeholder: getPlaceholder(),
		required,
		size: `size-${ size }`,
		type: 'tel',
		value: phoneNumber.value,
		wrapperClasses: classnames( {
			'gform-phone__input-wrapper': true,
		}, inputAttributes?.wrapperClasses || [] ),
	};

	const helpTextProps = {
		size: 'text-xs',
		weight: 'regular',
		...helpTextAttributes,
		customClasses: classnames( [ 'gform-phone__help-text' ], helpTextClasses ),
		id: helpTextId,
	};

	return (
		<div { ...componentProps } ref={ wrapperRef }>
			{ label && <div { ...labelProps }>{ label }{ required && <HelpText { ...requiredLabelProps } /> }</div> }
			<div className="gform-phone__wrapper">
				<CountryDropdown { ...dropdownProps } ref={ dropdownRef } />
				<Input { ...inputProps } ref={ inputRef } />
			</div>
			{ borderStyle === BORDER_STYLE_ERROR && <HelpText { ...helpTextProps } /> }
		</div>
	);
} );

PhoneComponent.propTypes = {
	countries: PropTypes.array,
	customAttributes: PropTypes.object,
	customClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	disabled: PropTypes.bool,
	dropdownAttributes: PropTypes.object,
	dropdownClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	helpTextAttributes: PropTypes.object,
	helpTextClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	i18n: PropTypes.object,
	id: PropTypes.string,
	inputAttributes: PropTypes.object,
	inputClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	international: PropTypes.bool,
	label: PropTypes.string,
	labelAttributes: PropTypes.object,
	labelClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	language: PropTypes.string,
	onChange: PropTypes.func,
	preferredCountries: PropTypes.array,
	required: PropTypes.bool,
	requiredLabelAttributes: PropTypes.object,
	requiredLabelClasses: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.array,
		PropTypes.object,
	] ),
	size: PropTypes.oneOf( [ 'r', 'l', 'xl' ] ),
	spacing: PropTypes.oneOfType( [
		PropTypes.string,
		PropTypes.number,
		PropTypes.array,
		PropTypes.object,
	] ),
	usePlaceholder: PropTypes.bool,
	useValidation: PropTypes.bool,
	width: PropTypes.number,
};

const PhoneComponentWithId = forwardRef( ( props, ref ) => {
	const defaultProps = {
		countries: [],
		customAttributes: {},
		customClasses: [],
		disabled: false,
		dropdownAttributes: {},
		dropdownClasses: [],
		helpTextAttributes: {},
		helpTextClasses: [],
		i18n: {},
		id: '',
		inputAttributes: {},
		inputClasses: [],
		international: true,
		label: '',
		labelAttributes: {},
		labelClasses: [],
		language: 'en',
		onChange: () => {},
		preferredCountries: [],
		required: false,
		requiredLabelAttributes: {},
		requiredLabelClasses: [],
		size: 'r',
		spacing: '',
		usePlaceholder: true,
		useValidation: false,
		width: 0,
	};
	const combinedProps = { ...defaultProps, ...props };
	const { id: idProp } = combinedProps;
	const idProviderProps = { id: idProp };

	return (
		<IdProvider { ...idProviderProps }>
			<PhoneComponent { ...combinedProps } ref={ ref } />
		</IdProvider>
	);
} );

export default PhoneComponentWithId;
