/**
 * External dependencies
 */
import { __ } from '@wordpress/i18n';
import {
	useState,
	useEffect,
	useCallback,
	useMemo,
	useRef,
} from '@wordpress/element';
import PropTypes from 'prop-types';
import classnames from 'classnames';
import FormattedMonetaryAmount from '@woocommerce/base-components/formatted-monetary-amount';
import { isObject } from '@woocommerce/types';

/**
 * Internal dependencies
 */
import './style.scss';
import { constrainRangeSliderValues } from './constrain-range-slider-values';
import FilterSubmitButton from '../filter-submit-button';

/**
 * Price slider component.
 *
 * @param {Object} props Component props.
 * @param {number} props.minPrice Minimum price for slider.
 * @param {number} props.maxPrice Maximum price for slider.
 * @param {number} props.minConstraint Minimum constraint.
 * @param {number} props.maxConstraint Maximum constraint.
 * @param {function(any):any} props.onChange Function to call on the change event.
 * @param {number} props.step What step values the slider uses.
 * @param {Object} props.currency Currency configuration object.
 * @param {boolean} props.showInputFields Whether to show input fields for the values or not.
 * @param {boolean} props.showFilterButton Whether to show the filter button for the slider.
 * @param {boolean} props.isLoading Whether values are loading or not.
 * @param {function():any} props.onSubmit Function to call when submit event fires.
 */
const PriceSlider = ( {
	minPrice,
	maxPrice,
	minConstraint,
	maxConstraint,
	onChange = () => {},
	step,
	currency,
	showInputFields = true,
	showFilterButton = false,
	isLoading = false,
	onSubmit = () => {},
} ) => {
	const minRange = useRef();
	const maxRange = useRef();

	// We want step to default to 10 major units, e.g. $10.
	const stepValue = step ? step : 10 * 10 ** currency.minorUnit;

	const [ minPriceInput, setMinPriceInput ] = useState( minPrice );
	const [ maxPriceInput, setMaxPriceInput ] = useState( maxPrice );

	useEffect( () => {
		setMinPriceInput( minPrice );
	}, [ minPrice ] );

	useEffect( () => {
		setMaxPriceInput( maxPrice );
	}, [ maxPrice ] );

	/**
	 * Checks if the min and max constraints are valid.
	 */
	const hasValidConstraints = useMemo( () => {
		return isFinite( minConstraint ) && isFinite( maxConstraint );
	}, [ minConstraint, maxConstraint ] );

	/**
	 * Handles styles for the shaded area of the range slider.
	 */
	const progressStyles = useMemo( () => {
		if (
			! isFinite( minPrice ) ||
			! isFinite( maxPrice ) ||
			! hasValidConstraints
		) {
			return {
				'--low': '0%',
				'--high': '100%',
			};
		}

		const low =
			Math.round(
				100 *
					( ( minPrice - minConstraint ) /
						( maxConstraint - minConstraint ) )
			) - 0.5;
		const high =
			Math.round(
				100 *
					( ( maxPrice - minConstraint ) /
						( maxConstraint - minConstraint ) )
			) + 0.5;

		return {
			'--low': low + '%',
			'--high': high + '%',
		};
	}, [
		minPrice,
		maxPrice,
		minConstraint,
		maxConstraint,
		hasValidConstraints,
	] );

	/**
	 * Works around an IE issue where only one range selector is visible by changing the display order
	 * based on the mouse position.
	 *
	 * @param {Object} event event data.
	 */
	const findClosestRange = useCallback(
		( event ) => {
			if ( isLoading || ! hasValidConstraints ) {
				return;
			}
			const bounds = event.target.getBoundingClientRect();
			const x = event.clientX - bounds.left;
			const minWidth = minRange.current.offsetWidth;
			const minValue = minRange.current.value;
			const maxWidth = maxRange.current.offsetWidth;
			const maxValue = maxRange.current.value;

			const minX = minWidth * ( minValue / maxConstraint );
			const maxX = maxWidth * ( maxValue / maxConstraint );

			const minXDiff = Math.abs( x - minX );
			const maxXDiff = Math.abs( x - maxX );

			/**
			 * The default z-index in the stylesheet as 20. 20 vs 21 is just for determining which range
			 * slider should be at the front and has no meaning beyond
			 */
			if ( minXDiff > maxXDiff ) {
				minRange.current.style.zIndex = 20;
				maxRange.current.style.zIndex = 21;
			} else {
				minRange.current.style.zIndex = 21;
				maxRange.current.style.zIndex = 20;
			}
		},
		[ isLoading, maxConstraint, hasValidConstraints ]
	);

	/**
	 * Called when the slider is dragged.
	 *
	 * @param {Object} event Event object.
	 */
	const rangeInputOnChange = useCallback(
		( event ) => {
			const isMin = event.target.classList.contains(
				'wc-block-price-filter__range-input--min'
			);
			const targetValue = event.target.value;
			const currentValues = isMin
				? [
						Math.round( targetValue / stepValue ) * stepValue,
						maxPrice,
				  ]
				: [
						minPrice,
						Math.round( targetValue / stepValue ) * stepValue,
				  ];
			const values = constrainRangeSliderValues(
				currentValues,
				minConstraint,
				maxConstraint,
				stepValue,
				isMin
			);
			onChange( [
				parseInt( values[ 0 ], 10 ),
				parseInt( values[ 1 ], 10 ),
			] );
		},
		[
			onChange,
			minPrice,
			maxPrice,
			minConstraint,
			maxConstraint,
			stepValue,
		]
	);

	/**
	 * Called when a price input loses focus - commit changes to slider.
	 *
	 * @param {Object} event Event object.
	 */
	const priceInputOnBlur = useCallback(
		( event ) => {
			// Only refresh when finished editing the min and max fields.
			if (
				event.relatedTarget &&
				event.relatedTarget.classList &&
				event.relatedTarget.classList.contains(
					'wc-block-price-filter__amount'
				)
			) {
				return;
			}
			const isMin = event.target.classList.contains(
				'wc-block-price-filter__amount--min'
			);
			const values = constrainRangeSliderValues(
				[ minPriceInput, maxPriceInput ],
				null,
				null,
				stepValue,
				isMin
			);
			onChange( [
				parseInt( values[ 0 ], 10 ),
				parseInt( values[ 1 ], 10 ),
			] );
		},
		[ onChange, stepValue, minPriceInput, maxPriceInput ]
	);

	const classes = classnames(
		'wc-block-price-filter',
		'wc-block-components-price-slider',
		showInputFields && 'wc-block-price-filter--has-input-fields',
		showInputFields && 'wc-block-components-price-slider--has-input-fields',
		showFilterButton && 'wc-block-price-filter--has-filter-button',
		showFilterButton &&
			'wc-block-components-price-slider--has-filter-button',
		isLoading && 'is-loading',
		! hasValidConstraints && 'is-disabled'
	);

	const activeElement = isObject( minRange.current )
		? minRange.current.ownerDocument.activeElement
		: undefined;
	const minRangeStep =
		activeElement && activeElement === minRange.current ? stepValue : 1;
	const maxRangeStep =
		activeElement && activeElement === maxRange.current ? stepValue : 1;

	const ariaReadableMinPrice = minPriceInput / 10 ** currency.minorUnit;
	const ariaReadableMaxPrice = maxPriceInput / 10 ** currency.minorUnit;

	return (
		<div className={ classes }>
			<div
				className="wc-block-price-filter__range-input-wrapper wc-block-components-price-slider__range-input-wrapper"
				onMouseMove={ findClosestRange }
				onFocus={ findClosestRange }
			>
				{ hasValidConstraints && (
					<div aria-hidden={ showInputFields }>
						<div
							className="wc-block-price-filter__range-input-progress wc-block-components-price-slider__range-input-progress"
							style={ progressStyles }
						/>
						<input
							type="range"
							className="wc-block-price-filter__range-input wc-block-price-filter__range-input--min wc-block-components-price-slider__range-input wc-block-components-price-slider__range-input--min"
							aria-label={ __(
								'Filter products by minimum price',
								'woocommerce'
							) }
							aria-valuetext={ ariaReadableMinPrice }
							value={
								Number.isFinite( minPrice )
									? minPrice
									: minConstraint
							}
							onChange={ rangeInputOnChange }
							step={ minRangeStep }
							min={ minConstraint }
							max={ maxConstraint }
							ref={ minRange }
							disabled={ isLoading }
							tabIndex={ showInputFields ? '-1' : '0' }
						/>
						<input
							type="range"
							className="wc-block-price-filter__range-input wc-block-price-filter__range-input--max wc-block-components-price-slider__range-input wc-block-components-price-slider__range-input--max"
							aria-label={ __(
								'Filter products by maximum price',
								'woocommerce'
							) }
							aria-valuetext={ ariaReadableMaxPrice }
							value={
								Number.isFinite( maxPrice )
									? maxPrice
									: maxConstraint
							}
							onChange={ rangeInputOnChange }
							step={ maxRangeStep }
							min={ minConstraint }
							max={ maxConstraint }
							ref={ maxRange }
							disabled={ isLoading }
							tabIndex={ showInputFields ? '-1' : '0' }
						/>
					</div>
				) }
			</div>
			<div className="wc-block-price-filter__controls wc-block-components-price-slider__controls">
				{ showInputFields && (
					<>
						<FormattedMonetaryAmount
							currency={ currency }
							displayType="input"
							className="wc-block-price-filter__amount wc-block-price-filter__amount--min wc-block-form-text-input wc-block-components-price-slider__amount wc-block-components-price-slider__amount--min"
							aria-label={ __(
								'Filter products by minimum price',
								'woocommerce'
							) }
							onValueChange={ ( value ) => {
								if ( value === minPriceInput ) {
									return;
								}
								setMinPriceInput( value );
							} }
							onBlur={ priceInputOnBlur }
							disabled={ isLoading || ! hasValidConstraints }
							value={ minPriceInput }
						/>
						<FormattedMonetaryAmount
							currency={ currency }
							displayType="input"
							className="wc-block-price-filter__amount wc-block-price-filter__amount--max wc-block-form-text-input wc-block-components-price-slider__amount wc-block-components-price-slider__amount--max"
							aria-label={ __(
								'Filter products by maximum price',
								'woocommerce'
							) }
							onValueChange={ ( value ) => {
								if ( value === maxPriceInput ) {
									return;
								}
								setMaxPriceInput( value );
							} }
							onBlur={ priceInputOnBlur }
							disabled={ isLoading || ! hasValidConstraints }
							value={ maxPriceInput }
						/>
					</>
				) }
				{ ! showInputFields &&
					! isLoading &&
					Number.isFinite( minPrice ) &&
					Number.isFinite( maxPrice ) && (
						<div className="wc-block-price-filter__range-text wc-block-components-price-slider__range-text">
							{ __( 'Price', 'woocommerce' ) }
							: &nbsp;
							<FormattedMonetaryAmount
								currency={ currency }
								value={ minPrice }
							/>
							&nbsp;&ndash;&nbsp;
							<FormattedMonetaryAmount
								currency={ currency }
								value={ maxPrice }
							/>
						</div>
					) }
				{ showFilterButton && (
					<FilterSubmitButton
						className="wc-block-price-filter__button wc-block-components-price-slider__button"
						disabled={ isLoading || ! hasValidConstraints }
						onClick={ onSubmit }
						screenReaderLabel={ __(
							'Apply price filter',
							'woocommerce'
						) }
					/>
				) }
			</div>
		</div>
	);
};

PriceSlider.propTypes = {
	/**
	 * Callback fired when prices changes.
	 */
	onChange: PropTypes.func.isRequired,
	/**
	 * Callback fired when the filter button is pressed.
	 */
	onSubmit: PropTypes.func,
	/**
	 * Min value.
	 */
	minPrice: PropTypes.number,
	/**
	 * Max value.
	 */
	maxPrice: PropTypes.number,
	/**
	 * Minimum allowed price.
	 */
	minConstraint: PropTypes.number,
	/**
	 * Maximum allowed price.
	 */
	maxConstraint: PropTypes.number,
	/**
	 * Step for slider inputs.
	 */
	step: PropTypes.number,
	/**
	 * Currency data used for formatting prices.
	 */
	currency: PropTypes.object.isRequired,
	/**
	 * Whether or not to show input fields above the slider.
	 */
	showInputFields: PropTypes.bool,
	/**
	 * Whether or not to show filter button above the slider.
	 */
	showFilterButton: PropTypes.bool,
	/**
	 * Whether or not to show filter button above the slider.
	 */
	isLoading: PropTypes.bool,
};

export default PriceSlider;
