/**
 * WordPress dependencies
 */
import { useEffect, useRef, useState } from '@safe-wordpress/element';
import { _x, sprintf } from '@safe-wordpress/i18n';

/**
 * External dependencies
 */
import { findIndex, findLastIndex, zip } from 'lodash';
import { usePluginSetting } from '@nab/data';
import {
	getLetter,
	sum,
	transformAlternativeChancesToAllocatedSlots,
} from '@nab/utils';
import type { Alternative, Maybe } from '@nab/types';

/**
 * Internal dependencies
 */
import './style.scss';
import { Tooltip } from '../tooltip';

export type AlternativeDistributionSettingProps = {
	readonly alternatives: ReadonlyArray< Alternative >;
	readonly updateCuts?: ( alternatives: ReadonlyArray< number > ) => void;
};

export const AlternativeDistributionSetting = ( {
	alternatives,
	updateCuts,
}: AlternativeDistributionSettingProps ): JSX.Element | null => {
	const visible = usePluginSetting( 'isAlternativeDistributionAllowed' );

	const maxCombinations = usePluginSetting( 'maxCombinations' );

	const barRef = useRef< HTMLDivElement | null >( null );
	const [ barWidth, setBarWidth ] = useState( 0 );

	useEffect( () => {
		const element = barRef.current;

		if ( ! element ) {
			return;
		}

		const updateWidth = () => {
			setBarWidth( element.getBoundingClientRect().width );
		};

		updateWidth();

		const resizeObserver = new ResizeObserver( updateWidth );
		resizeObserver.observe( element );

		return () => {
			resizeObserver.disconnect();
		};
	}, [] );

	const sizes = getDistribution( alternatives, maxCombinations );
	const allocatedSlots = transformAlternativeChancesToAllocatedSlots(
		sizes,
		maxCombinations
	);
	if ( ! visible || allocatedSlots.length !== alternatives.length ) {
		return null;
	}

	const alternativeIds = alternatives.map( ( a ) => a.id );
	const data = zip( sizes, alternativeIds, allocatedSlots );

	const onMoveLeft = ( index: number ) => {
		if ( ! updateCuts ) {
			return undefined;
		}

		if ( index >= data.length - 1 ) {
			return undefined;
		}

		const left = allocatedSlots.slice( 0, index + 1 );
		if ( sum( left ) <= left.length ) {
			return undefined;
		}

		return () => {
			const lastIndex = findLastIndex( left, ( v ) => v > 1 );
			const newAllocatedSlots = [ ...allocatedSlots ];
			// @ts-expect-error Index is within range.
			newAllocatedSlots[ lastIndex ]--;
			// @ts-expect-error Index is within range.
			newAllocatedSlots[ index + 1 ]++;
			updateCuts( newAllocatedSlots );
		};
	};

	const onMoveRightIndex = ( index: number ) => {
		if ( ! updateCuts ) {
			return undefined;
		}

		if ( index >= data.length - 1 ) {
			return undefined;
		}

		const right = allocatedSlots.slice( index + 1, allocatedSlots.length );
		if ( sum( right ) <= right.length ) {
			return undefined;
		}

		return () => {
			const firstIndex = findIndex( right, ( v ) => v > 1 );
			const newAllocatedSlots = [ ...allocatedSlots ];
			// @ts-expect-error Index is within range.
			newAllocatedSlots[ index ]++;
			// @ts-expect-error Index is within range.
			newAllocatedSlots[ index + firstIndex + 1 ]--;
			updateCuts( newAllocatedSlots );
		};
	};

	return (
		<div className="nab-alternative-distribution-setting">
			<div
				ref={ barRef }
				className="nab-alternative-distribution-setting__distribution-bar"
			>
				{ data.map( ( [ size, id, cut ], index ) => (
					<DistributionBar
						key={ id }
						letter={ getLetter( index ) }
						label={ sprintf(
							/* translators: %1$s: Variant letter. %2$s: Chance percentage. %3$s: Chance ratio. */
							_x(
								'Variant %1$s: %2$s%% (%3$s)',
								'text (alternative distribution)',
								'nelio-ab-testing'
							),
							getLetter( index ),
							`${ size?.toFixed( 2 ) }`,
							`${ cut }/${ maxCombinations }`
						) }
						step={ Math.round( barWidth / maxCombinations ) }
						width={ size }
						onMoveLeft={ onMoveLeft( index ) }
						onMoveRight={ onMoveRightIndex( index ) }
					/>
				) ) }
			</div>
		</div>
	);
};

const DistributionBar = ( {
	label,
	letter,
	step,
	width,
	onMoveLeft,
	onMoveRight,
}: {
	readonly label: string;
	readonly letter: string;
	readonly step: number;
	readonly width: Maybe< number >;
	readonly onMoveRight: Maybe< () => void >;
	readonly onMoveLeft: Maybe< () => void >;
} ) => {
	const handleRef = useRef< HTMLDivElement | null >( null );
	const hasHandle = !! onMoveLeft || !! onMoveRight;

	const dragStartXRef = useRef< number | null >( null );
	const dragOffsetRef = useRef( 0 );

	const handleKeyDown = ( event: React.KeyboardEvent< HTMLDivElement > ) => {
		switch ( event.key ) {
			case 'ArrowLeft':
			case '-':
				event.preventDefault();
				return onMoveLeft?.();

			case 'ArrowRight':
			case '+':
				event.preventDefault();
				return onMoveRight?.();
		}
	};

	function handlePointerDown( event: React.PointerEvent< HTMLDivElement > ) {
		dragStartXRef.current = event.clientX;
		dragOffsetRef.current = 0;

		event.currentTarget.setPointerCapture( event.pointerId );
	}

	function handlePointerMove( event: React.PointerEvent< HTMLDivElement > ) {
		if ( dragStartXRef.current === null ) {
			return;
		}

		const currentX = event.clientX;
		const totalDistance = currentX - dragStartXRef.current;

		const pendingOffset = totalDistance - dragOffsetRef.current;

		if ( pendingOffset >= step ) {
			const steps = Math.floor( pendingOffset / step );

			for ( let i = 0; i < steps; i += 1 ) {
				onMoveRight?.();
			}

			dragOffsetRef.current += steps * step;
		}

		if ( pendingOffset <= -step ) {
			const steps = Math.floor( Math.abs( pendingOffset ) / step );

			for ( let i = 0; i < steps; i += 1 ) {
				onMoveLeft?.();
			}

			dragOffsetRef.current -= steps * step;
		}
	}

	function handlePointerUp( event: React.PointerEvent< HTMLDivElement > ) {
		dragStartXRef.current = null;
		dragOffsetRef.current = 0;

		event.currentTarget.releasePointerCapture( event.pointerId );
	}

	return (
		<div
			className="nab-alternative-distribution-setting__variant"
			style={ { position: 'relative', width: `${ width }%` } }
		>
			<Tooltip text={ label }>
				<div className="nab-alternative-distribution-setting__variant-tooltip"></div>
			</Tooltip>
			{ hasHandle && (
				<div
					ref={ handleRef }
					className="nab-alternative-distribution-setting__variant-handler"
					role="slider"
					tabIndex={ 0 }
					aria-label={ sprintf(
						/* translators: %s: Variant letter. */
						_x(
							'Resize variant %s',
							'command',
							'nelio-ab-testing'
						),
						letter
					) }
					aria-orientation="horizontal"
					onKeyDown={ handleKeyDown }
					onPointerDown={ handlePointerDown }
					onPointerMove={ handlePointerMove }
					onPointerUp={ handlePointerUp }
					onPointerCancel={ handlePointerUp }
				></div>
			) }
		</div>
	);
};

// =======
// HELPERS
// =======

function getDistribution(
	alternatives: ReadonlyArray< Alternative >,
	slots: number
): ReadonlyArray< number > {
	const [ control, ...rest ] = alternatives;

	const minPercentage = ( 1 / slots ) * 100;
	const restSizes = rest.map( ( a ) =>
		Math.max(
			'number' === typeof a.attributes.chance ? a.attributes.chance : 0,
			minPercentage
		)
	);

	const aChance = control?.attributes.chance;
	const bChance = rest[ 0 ]?.attributes.chance;
	const controlSize =
		( 'number' === typeof aChance ? aChance : 0 ) ||
		( 'number' === typeof bChance
			? Math.max( 100 - sum( restSizes ), minPercentage )
			: minPercentage );

	const sizes = [ controlSize, ...restSizes ];
	const allocatedSlots = transformAlternativeChancesToAllocatedSlots(
		sizes,
		slots
	);
	return allocatedSlots.map( ( c ) => ( c / slots ) * 100 );
}
