import { useEffect, useMemo, useRef, useState } from 'react';
import { useMergedRefs } from '@base-ui/utils/useMergedRefs';
import { bigIntToNumber, formatBigInt, numberToBigInt, parseBigInt, } from '@graphprotocol/gds-utils';
import { useForceRerender } from "./useForceRerender.js";
import { usePrevious } from "./usePrevious.js";
export function useNumberInput({ ref: passedRef, useBigInt = false, precision = 18n, defaultValue = null, value: controlledValue, onChange: controlledOnChange, required = false, min: passedMin = null, max: passedMax = null, format = {}, } = {}) {
    if (passedMin !== null && passedMax !== null && passedMin > passedMax) {
        throw new Error('[useNumberInput] `min` cannot be greater than `max`');
    }
    const inputRef = useRef(null);
    const inputPassedRef = useMergedRefs(inputRef, passedRef);
    const normalizeValue = (value) => {
        if (typeof value === 'number') {
            return numberToBigInt(value, { precision });
        }
        else if (typeof value === 'bigint') {
            return value;
        }
        else {
            return null;
        }
    };
    const denormalizeValue = (value) => {
        if (value === null) {
            return null;
        }
        else if (useBigInt) {
            return value;
        }
        else {
            return bigIntToNumber(value, { precision });
        }
    };
    const formatValue = (value, overrides) => {
        if (value === null)
            return '';
        return formatBigInt(value, { precision, ...format, ...overrides });
    };
    const defaultMin = useBigInt ? null : Number.MIN_SAFE_INTEGER;
    const defaultMax = useBigInt ? null : Number.MAX_SAFE_INTEGER;
    const min = normalizeValue(passedMin ?? defaultMin);
    const max = normalizeValue(passedMax ?? defaultMax);
    const allowNegative = min === null || min < 0n;
    const allowDecimals = precision > 0n;
    const validateValue = (value) => {
        if (value === null) {
            if (required) {
                return 'EMPTY';
            }
            return null;
        }
        if (min !== null && value < min) {
            return 'BELOW_MIN';
        }
        if (max !== null && value > max) {
            return 'ABOVE_MAX';
        }
        return null;
    };
    const derivedFromProps = (() => {
        const normalizedValue = normalizeValue(controlledValue !== undefined ? controlledValue : defaultValue);
        const error = validateValue(normalizedValue);
        const validatedValue = !error ? normalizedValue : null;
        return {
            normalizedValue,
            error,
            validatedValue,
        };
    })();
    const [typing, setTyping] = useState(false);
    const [inputValue, setInputValue] = useState(() => formatValue(derivedFromProps.normalizedValue));
    const [validatedValue, setValidatedValue] = useState(derivedFromProps.validatedValue);
    const [error, setError] = useState(derivedFromProps.error !== 'EMPTY' ? derivedFromProps.error : null);
    const [errorOnBlur, setErrorOnBlur] = useState(error);
    const [nonValidatedValue, setNonValidatedValue] = useState(derivedFromProps.normalizedValue);
    let nearestValidValue = nonValidatedValue ?? 0n;
    if (min !== null && (nonValidatedValue ?? 0n) < min) {
        nearestValidValue = min;
    }
    if (max !== null && (nonValidatedValue ?? 0n) > max) {
        nearestValidValue = max;
    }
    const setValue = (newValue, { updateInputValue = true, callOnChange = false, } = {}) => {
        const normalizedValue = normalizeValue(newValue);
        const formattedValue = formatValue(normalizedValue);
        const newError = validateValue(normalizedValue);
        if (updateInputValue)
            setInputValue(formattedValue);
        setNonValidatedValue(normalizedValue);
        if (normalizedValue === null || newError) {
            setValidatedValue(null);
            if (callOnChange)
                controlledOnChange?.(null);
            setError(newError);
            return;
        }
        // The passed value may have more decimals than we allow, so we parse the formatted value to truncate those
        const parsedValue = parseBigInt(formattedValue, { precision });
        setValidatedValue(parsedValue);
        if (callOnChange) {
            ;
            controlledOnChange?.(denormalizeValue(parsedValue));
        }
        setError(null);
    };
    // Update internal values when a prop changes externally that might affect the value or the error
    const previousControlledValue = usePrevious(controlledValue);
    const previousRequired = usePrevious(required);
    const previousMin = usePrevious(passedMin);
    const previousMax = usePrevious(passedMax);
    if ((previousControlledValue.hasChanged &&
        (controlledValue === undefined || normalizeValue(controlledValue) !== validatedValue)) ||
        previousRequired.hasChanged ||
        previousMin.hasChanged ||
        previousMax.hasChanged) {
        if (previousControlledValue.hasChanged && controlledValue !== undefined) {
            setValue(controlledValue);
        }
        else {
            setValue(denormalizeValue(nonValidatedValue));
        }
    }
    const getSignificantCaretPositionMap = (string) => {
        const significantCaretPositionMap = [];
        let significantCaretPosition = 0;
        let hasDecimalPoint = false;
        let hasSignificantDigit = false;
        significantCaretPositionMap.push(significantCaretPosition);
        for (let i = 0; i < string.length; i++) {
            const character = string[i];
            if (character === '-' && significantCaretPosition === 0) {
                significantCaretPosition++;
            }
            else if (character === '.' && !hasDecimalPoint) {
                significantCaretPosition++;
                hasDecimalPoint = true;
                // Handle typing `.5` (instead of `0.5`)
                if (string[i - 1] !== '0') {
                    significantCaretPosition++;
                }
            }
            else if (/\d/.test(character)) {
                if (character === '0' && !hasSignificantDigit) {
                    if (string[i - 1] === '.' || string[i + 1] === '.') {
                        significantCaretPosition++;
                        hasSignificantDigit = true;
                    }
                }
                else {
                    significantCaretPosition++;
                    hasSignificantDigit = true;
                }
            }
            significantCaretPositionMap.push(significantCaretPosition);
        }
        return significantCaretPositionMap;
    };
    const getSignificantCaretPosition = (input) => {
        const significantCaretPositionMap = getSignificantCaretPositionMap(input.value);
        return significantCaretPositionMap[input.selectionStart !== null && input.selectionStart < significantCaretPositionMap.length
            ? input.selectionStart
            : significantCaretPositionMap.length];
    };
    const setSignificantCaretPosition = (input, position) => {
        const significantCaretPositionMap = getSignificantCaretPositionMap(input.value);
        let actualCaretPosition = significantCaretPositionMap.indexOf(position);
        if (actualCaretPosition === -1) {
            actualCaretPosition = input.value.length;
        }
        input.setSelectionRange(actualCaretPosition, actualCaretPosition);
    };
    const previousSignificantCaretPosition = useRef(null);
    const [forceRerender] = useForceRerender();
    const charsToRemove = useMemo(() => new RegExp(`[^\\d,${allowNegative ? '-' : ''}${allowDecimals ? '.' : ''}]`, 'g'), [allowNegative, allowDecimals]);
    const onKeyDown = (event) => {
        const allowKeyPress = event.ctrlKey ||
            event.metaKey ||
            /\d/.test(event.key) ||
            (allowNegative && event.key === '-') ||
            (allowDecimals && event.key === '.') ||
            // TODO: Fix stuck caret when pressing Delete to the left of a comma
            // TODO: Fix caret sometimes not ending up in the correct position when pressing Delete to the left of the decimal point
            ['Backspace', 'Delete', 'ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown', 'Tab'].includes(event.key);
        if (!allowKeyPress) {
            event.preventDefault();
        }
    };
    const onChange = (newInputValue) => {
        setTyping(true);
        if (inputRef.current) {
            previousSignificantCaretPosition.current = getSignificantCaretPosition(inputRef.current);
        }
        // Remove all characters except digits, commas, minus signs (if allowed), and decimal points (if allowed)
        let cleanedValue = newInputValue.replace(charsToRemove, '');
        // Remove minus signs that are not at the beginning of the string
        if (allowNegative) {
            cleanedValue = cleanedValue.replace(/(?!^)-/g, '');
        }
        // Remove decimal points except the first one
        if (allowDecimals) {
            const cleanedValueParts = cleanedValue.split('.');
            cleanedValue =
                cleanedValueParts.length > 1
                    ? `${cleanedValueParts.shift()}.${cleanedValueParts.join('')}`
                    : cleanedValueParts[0];
        }
        setInputValue(cleanedValue);
        if (cleanedValue === '') {
            setValue(null, {
                updateInputValue: false,
                callOnChange: true,
            });
            return;
        }
        const parsedValue = parseBigInt(cleanedValue, { precision });
        if (parsedValue === null) {
            setValidatedValue(null);
            controlledOnChange?.(null);
            setError('NOT_A_NUMBER');
            return;
        }
        setValue(parsedValue, {
            updateInputValue: false,
            callOnChange: true,
        });
        // Don't format potentially intermediate values like `-0` (in case we're typing `-0.1`), `1.` (for `1.5`), or `1.50` (for `1.501`)
        // Those cases will be formatted on blur instead
        if (!/^-0+$/.test(cleanedValue) && !/\.((.*)0+)?$/.test(cleanedValue)) {
            setInputValue(formatValue(parsedValue, { minimumFractionDigits: 0, maximumFractionDigits: 20 }));
        }
        // Ensure the caret position is updated correctly when pressing backspace on a comma
        forceRerender();
    };
    // Ensure the caret is in the correct position after the value changes
    useEffect(() => {
        if (!inputRef.current || previousSignificantCaretPosition.current === null)
            return;
        const significantCaretPosition = getSignificantCaretPosition(inputRef.current);
        if (significantCaretPosition !== previousSignificantCaretPosition.current) {
            setSignificantCaretPosition(inputRef.current, previousSignificantCaretPosition.current);
        }
        previousSignificantCaretPosition.current = null;
    });
    const onBlur = () => {
        setTyping(false);
        setErrorOnBlur(error);
        const parsedValue = parseBigInt(inputValue, { precision });
        if (parsedValue !== null) {
            setInputValue(formatValue(parsedValue));
        }
    };
    const status = error ? 'error' : undefined;
    let message;
    if (error) {
        switch (error) {
            case 'NOT_A_NUMBER':
                message = 'Please enter a valid number.';
                break;
            case 'EMPTY':
                message = 'This field is required.';
                break;
            case 'BELOW_MIN':
                message = `This field's value cannot be less than ${formatValue(min)}.`;
                break;
            case 'ABOVE_MAX':
                message = `This field's value cannot be greater than ${formatValue(max)}.`;
                break;
        }
    }
    // Only show the error message when not typing, or while typing until the input is valid again
    if (typing && error !== errorOnBlur) {
        message = undefined;
    }
    return {
        value: denormalizeValue(validatedValue),
        setValue: setValue,
        error,
        nonValidatedValue,
        nearestValidValue: denormalizeValue(nearestValidValue),
        props: {
            ref: inputPassedRef,
            type: 'number',
            value: inputValue,
            onKeyDown,
            onChange,
            onBlur,
            required,
            min: min !== null ? (bigIntToNumber(min, { precision }) ?? undefined) : undefined,
            max: max !== null ? (bigIntToNumber(max, { precision }) ?? undefined) : undefined,
            status,
            message,
        },
    };
}
//# sourceMappingURL=useNumberInput.js.map