/**
 * GeChiUI dependencies
 */
import { __, _x } from '@gechiui/i18n';
import { Platform } from '@gechiui/element';

/**
 * Internal dependencies
 */
import type { Value, GCUnitControlUnit, GCUnitControlUnitList } from './types';

const isWeb = Platform.OS === 'web';

const allUnits: Record< string, GCUnitControlUnit > = {
	px: {
		value: 'px',
		label: isWeb ? 'px' : __( 'Pixels (px)' ),
		default: '',
		a11yLabel: __( 'Pixels (px)' ),
		step: 1,
	},
	'%': {
		value: '%',
		label: isWeb ? '%' : __( 'Percentage (%)' ),
		default: '',
		a11yLabel: __( 'Percent (%)' ),
		step: 0.1,
	},
	em: {
		value: 'em',
		label: isWeb ? 'em' : __( 'Relative to parent font size (em)' ),
		default: '',
		a11yLabel: _x( 'ems', 'Relative to parent font size (em)' ),
		step: 0.01,
	},
	rem: {
		value: 'rem',
		label: isWeb ? 'rem' : __( 'Relative to root font size (rem)' ),
		default: '',
		a11yLabel: _x( 'rems', 'Relative to root font size (rem)' ),
		step: 0.01,
	},
	vw: {
		value: 'vw',
		label: isWeb ? 'vw' : __( 'Viewport width (vw)' ),
		default: '',
		a11yLabel: __( 'Viewport width (vw)' ),
		step: 0.1,
	},
	vh: {
		value: 'vh',
		label: isWeb ? 'vh' : __( 'Viewport height (vh)' ),
		default: '',
		a11yLabel: __( 'Viewport height (vh)' ),
		step: 0.1,
	},
	vmin: {
		value: 'vmin',
		label: isWeb ? 'vmin' : __( 'Viewport smallest dimension (vmin)' ),
		default: '',
		a11yLabel: __( 'Viewport smallest dimension (vmin)' ),
		step: 0.1,
	},
	vmax: {
		value: 'vmax',
		label: isWeb ? 'vmax' : __( 'Viewport largest dimension (vmax)' ),
		default: '',
		a11yLabel: __( 'Viewport largest dimension (vmax)' ),
		step: 0.1,
	},
	ch: {
		value: 'ch',
		label: isWeb ? 'ch' : __( 'Width of the zero (0) character (ch)' ),
		default: '',
		a11yLabel: __( 'Width of the zero (0) character (ch)' ),
		step: 0.01,
	},
	ex: {
		value: 'ex',
		label: isWeb ? 'ex' : __( 'x-height of the font (ex)' ),
		default: '',
		a11yLabel: __( 'x-height of the font (ex)' ),
		step: 0.01,
	},
	cm: {
		value: 'cm',
		label: isWeb ? 'cm' : __( 'Centimeters (cm)' ),
		default: '',
		a11yLabel: __( 'Centimeters (cm)' ),
		step: 0.001,
	},
	mm: {
		value: 'mm',
		label: isWeb ? 'mm' : __( 'Millimeters (mm)' ),
		default: '',
		a11yLabel: __( 'Millimeters (mm)' ),
		step: 0.1,
	},
	in: {
		value: 'in',
		label: isWeb ? 'in' : __( 'Inches (in)' ),
		default: '',
		a11yLabel: __( 'Inches (in)' ),
		step: 0.001,
	},
	pc: {
		value: 'pc',
		label: isWeb ? 'pc' : __( 'Picas (pc)' ),
		default: '',
		a11yLabel: __( 'Picas (pc)' ),
		step: 1,
	},
	pt: {
		value: 'pt',
		label: isWeb ? 'pt' : __( 'Points (pt)' ),
		default: '',
		a11yLabel: __( 'Points (pt)' ),
		step: 1,
	},
};

/**
 * An array of all available CSS length units.
 */
export const ALL_CSS_UNITS = Object.values( allUnits );

/**
 * Units of measurements. `a11yLabel` is used by screenreaders.
 */
export const CSS_UNITS = [
	allUnits.px,
	allUnits[ '%' ],
	allUnits.em,
	allUnits.rem,
	allUnits.vw,
	allUnits.vh,
];

export const DEFAULT_UNIT = allUnits.px;

/**
 * Handles legacy value + unit handling.
 * This component use to manage both incoming value and units separately.
 *
 * Moving forward, ideally the value should be a string that contains both
 * the value and unit, example: '10px'
 *
 * @param  value Value
 * @param  unit  Unit value
 * @param  units Units to derive from.
 * @return The extracted number and unit.
 */
export function getParsedValue(
	value: Value,
	unit?: string,
	units?: GCUnitControlUnitList
): [ Value, string | undefined ] {
	const initialValue = unit ? `${ value }${ unit }` : value;

	return parseUnit( initialValue, units );
}

/**
 * Checks if units are defined.
 *
 * @param  units Units to check.
 * @return Whether units are defined.
 */
export function hasUnits( units: GCUnitControlUnitList ): boolean {
	return Array.isArray( units ) && !! units.length;
}

/**
 * Parses a number and unit from a value.
 *
 * @param  initialValue Value to parse
 * @param  units        Units to derive from.
 * @return The extracted number and unit.
 */
export function parseUnit(
	initialValue: Value | undefined,
	units: GCUnitControlUnitList = ALL_CSS_UNITS
): [ Value, string | undefined ] {
	const value = String( initialValue ).trim();

	let num: Value = parseFloat( value );
	num = isNaN( num ) ? '' : num;

	const unitMatch = value.match( /[\d.\-\+]*\s*(.*)/ );

	let unit: string | undefined =
		unitMatch?.[ 1 ] !== undefined ? unitMatch[ 1 ] : '';
	unit = unit.toLowerCase();

	if ( hasUnits( units ) && units !== false ) {
		const match = units.find( ( item ) => item.value === unit );
		unit = match?.value;
	} else {
		unit = DEFAULT_UNIT.value;
	}

	return [ num, unit ];
}

/**
 * Parses a number and unit from a value. Validates parsed value, using fallback
 * value if invalid.
 *
 * @param  next          The next value.
 * @param  units         Units to derive from.
 * @param  fallbackValue The fallback value.
 * @param  fallbackUnit  The fallback value.
 * @return The extracted value and unit.
 */
export function getValidParsedUnit(
	next: Value | undefined,
	units: GCUnitControlUnitList,
	fallbackValue: Value,
	fallbackUnit: string | undefined
): [ Value, string | undefined ] {
	const [ parsedValue, parsedUnit ] = parseUnit( next, units );
	let baseValue = parsedValue;
	let baseUnit: string | undefined;

	// The parsed value from `parseUnit` should now be either a
	// real number or an empty string. If not, use the fallback value.
	if ( ! Number.isFinite( parsedValue ) || parsedValue === '' ) {
		baseValue = fallbackValue;
	}

	baseUnit = parsedUnit || fallbackUnit;

	/**
	 * If no unit is found, attempt to use the first value from the collection
	 * of units as a default fallback.
	 */
	if ( Array.isArray( units ) && hasUnits( units ) && ! baseUnit ) {
		baseUnit = units[ 0 ]?.value;
	}

	return [ baseValue, baseUnit ];
}

/**
 * Takes a unit value and finds the matching accessibility label for the
 * unit abbreviation.
 *
 * @param  unit Unit value (example: px)
 * @return a11y label for the unit abbreviation
 */
export function parseA11yLabelForUnit( unit: string ): string | undefined {
	const match = ALL_CSS_UNITS.find( ( item ) => item.value === unit );
	return match?.a11yLabel ? match?.a11yLabel : match?.value;
}

/**
 * Filters available units based on values defined by the unit setting/property.
 *
 * @param  unitSetting Collection of preferred unit value strings.
 * @param  units       Collection of available unit objects.
 *
 * @return Filtered units based on settings.
 */
export function filterUnitsWithSettings(
	unitSetting: Array< string > = [],
	units: GCUnitControlUnitList
): Array< GCUnitControlUnit > {
	return Array.isArray( units )
		? units.filter( ( unit ) => {
				return unitSetting.includes( unit.value );
		  } )
		: [];
}

/**
 * Custom hook to retrieve and consolidate units setting from add_theme_support().
 * TODO: ideally this hook shouldn't be needed
 * https://github.com/GeChiUI/gutenberg/pull/31822#discussion_r633280823
 *
 * @param  args                An object containing units, settingPath & defaultUnits.
 * @param  args.units          Collection of all potentially available units.
 * @param  args.availableUnits Collection of unit value strings for filtering available units.
 * @param  args.defaultValues  Collection of default values for defined units. Example: { px: '350', em: '15' }.
 *
 * @return Filtered units based on settings.
 */
export const useCustomUnits = ( {
	units,
	availableUnits,
	defaultValues,
}: {
	units?: GCUnitControlUnitList;
	availableUnits?: Array< string >;
	defaultValues: Record< string, Value >;
} ): GCUnitControlUnitList => {
	units = units || ALL_CSS_UNITS;
	const usedUnits = filterUnitsWithSettings(
		! availableUnits ? [] : availableUnits,
		units
	);

	if ( defaultValues ) {
		usedUnits.forEach( ( unit, i ) => {
			if ( defaultValues[ unit.value ] ) {
				usedUnits[ i ].default = defaultValues[ unit.value ];
			}
		} );
	}

	return usedUnits.length === 0 ? false : usedUnits;
};

/**
 * Get available units with the unit for the currently selected value
 * prepended if it is not available in the list of units.
 *
 * This is useful to ensure that the current value's unit is always
 * accurately displayed in the UI, even if the intention is to hide
 * the availability of that unit.
 *
 * @param  currentValue Selected value to parse.
 * @param  legacyUnit   Legacy unit value, if currentValue needs it appended.
 * @param  units        List of available units.
 *
 * @return A collection of units containing the unit for the current value.
 */
export function getUnitsWithCurrentUnit(
	currentValue: Value,
	legacyUnit: string | undefined,
	units: GCUnitControlUnitList = ALL_CSS_UNITS
): GCUnitControlUnitList {
	if ( ! Array.isArray( units ) ) {
		return units;
	}

	const unitsWithCurrentUnit = [ ...units ];
	const [ , currentUnit ] = getParsedValue(
		currentValue,
		legacyUnit,
		ALL_CSS_UNITS
	);

	if (
		currentUnit &&
		! unitsWithCurrentUnit.some( ( unit ) => unit.value === currentUnit )
	) {
		if ( allUnits[ currentUnit ] ) {
			unitsWithCurrentUnit.unshift( allUnits[ currentUnit ] );
		}
	}

	return unitsWithCurrentUnit;
}
