// https://technology.blog.gov.uk/2020/02/24/why-the-gov-uk-design-system-team-changed-the-input-type-for-numbers/
// https://gist.github.com/RobertAKARobin/850a408e04d5414e67d308a2b5847378
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, } from "react";
import { maxValue, minValue, optional, pipe, string, transform, } from "valibot";
import { useCombinedRefs } from "../../../hook/useCombinedRefs";
import { useInputValidation } from "../../../hook/useInputValidation";
import { createReducerHook } from "../../../utils";
import { setNativeValue, validateElement } from "../../../utils/form";
import { Input } from "../input";
import { Option, Select } from "../select";
import * as styles from "./date-input.css";
import { useMonths } from "./hooks";
import { getCurrentValue, getValues } from "./utils";
let DateInputCtx = createContext(null);
/**
 * Hook to access the parent DateInput's state.
 *
 * @throws Error if used outside of a DateInput component.
 * @returns The DateInputContext.
 */
function useDateInput() {
    let ctx = useContext(DateInputCtx);
    if (ctx === null) {
        throw new Error("DateInputCtx missing");
    }
    return ctx;
}
let useDateInputReducer = createReducerHook({
    initialState: {
        year: null,
        month: null,
        day: null,
    },
    reducers: {
        setYear(state, value) {
            state.year = value;
        },
        setMonth(state, value) {
            state.month = value;
        },
        setDay(state, value) {
            state.day = value;
        },
        setAll(_state, value) {
            return value;
        }
    }
});
/**
 * A compound component for selecting a date using separate inputs for day, month, and year.
 *
 * @remarks
 * This component manages the state of three separate inputs and combines them into a single
 * ISO date string (YYYY-MM-DD) in a hidden input for form submission.
 * It uses a reducer to manage internal state and synchronizes with the `value` prop.
 *
 * @param props - The component props.
 * @returns The rendered date input wrapper.
 *
 * @example
 * ```tsx
 * import { DateInput, Day, MonthSelect, Year } from './date_input';
 *
 * function BirthdayField() {
 *   return (
 *     <DateInput name="birthday" required>
 *       <Day placeholder="DD" />
 *       <MonthSelect />
 *       <Year placeholder="YYYY" />
 *     </DateInput>
 *   );
 * }
 * ```
 */
export function DateInput({ children, name, onChange, defaultValue, value, id, onValidationError, validate, required, onBlur, ...props }) {
    let defaultValueRef = useRef(defaultValue);
    let [currentYear, currentMonth, currentDay] = useMemo(() => {
        if (value) {
            return getValues(value);
        }
        if (defaultValueRef.current) {
            return getValues(defaultValueRef.current);
        }
        let currentDate = new Date();
        return [
            currentDate.getFullYear(),
            currentDate.getMonth(),
            currentDate.getDate(),
        ];
    }, [value]);
    let setChange = useCallback((value) => {
        if (inputRef.current) {
            setNativeValue(inputRef.current, value);
            inputRef.current.dispatchEvent(new Event("change", { bubbles: true }));
        }
    }, []);
    let [state, mutate] = useDateInputReducer(() => {
        return {
            year: currentYear,
            month: currentMonth,
            day: currentDay,
        };
    });
    let DAY = value ? currentDay ?? null : state.day;
    let MONTH = value ? (currentMonth ? currentMonth - 1 : null) : state.month;
    let YEAR = value ? currentYear ?? null : state.year;
    let dayRef = useRef(DAY);
    let monthRef = useRef(MONTH);
    let yearRef = useRef(YEAR);
    useEffect(() => {
        dayRef.current = value ? currentDay ?? null : state.day;
        monthRef.current = value ? (currentMonth ?? 1) - 1 : state.month;
        yearRef.current = value ? currentYear ?? null : state.year;
    });
    let handleSetDay = useCallback((day) => {
        let value = getCurrentValue(yearRef.current, monthRef.current, day);
        setChange(value);
        mutate.setDay(day);
    }, [setChange]);
    let handleSetMonth = useCallback((month) => {
        let value = getCurrentValue(yearRef.current, month, dayRef.current);
        setChange(value);
        mutate.setMonth(month);
    }, [setChange, mutate]);
    let handleSetYear = useCallback((year) => {
        let value = getCurrentValue(year, monthRef.current, dayRef.current);
        setChange(value);
        mutate.setYear(year);
    }, [setChange, mutate]);
    let inputRef = useRef(null);
    let currentValue = value === undefined ? getCurrentValue(YEAR, MONTH, DAY) : value;
    let wrapper = useRef(null);
    let resetValue = useRef(currentValue);
    let handleSetAll = useCallback(() => {
        let [year, m, day] = getValues(resetValue.current);
        let month = (m ?? 1) - 1;
        let value = getCurrentValue(year, month, day);
        setChange(value);
        mutate.setAll({ year, month, day });
    }, [setChange, mutate]);
    useEffect(() => {
        if (!wrapper.current) {
            return;
        }
        let form = wrapper.current.closest("form");
        if (!form) {
            return;
        }
        let handleReset = (_e) => {
            // I am currently not sure how to better force the month select to reset
            // properly, since I can't set value and defaultValue at the same time,
            // neither can I set the "selcted" property to an option element.
            // thus we let the browser reset select to the first option and then
            // overwrite the selected value. This causes a flicker in the UI
            requestAnimationFrame(() => {
                handleSetAll();
            });
        };
        form.addEventListener("reset", handleReset);
        return () => {
            form?.removeEventListener("reset", handleReset);
        };
    }, [handleSetAll]);
    let hiddenInputRef = useCombinedRefs(inputRef);
    let handleFocus = useCallback((e) => {
        if (wrapper.current === null) {
            return;
        }
        let inputs = Array.from(wrapper.current.querySelectorAll("input,select"));
        let firstInput = inputs.find((node) => node !== e?.target);
        if (!firstInput) {
            return;
        }
        firstInput.focus();
    }, []);
    let ctx = useMemo(() => {
        return {
            day: DAY,
            setDay: value ? handleSetDay : mutate.setDay,
            month: MONTH,
            setMonth: value ? handleSetMonth : mutate.setMonth,
            year: YEAR,
            setYear: value ? handleSetYear : mutate.setYear,
        };
    }, [DAY, value, handleSetDay, mutate, MONTH, handleSetMonth, handleSetYear, YEAR]);
    let validationHandlers = useInputValidation({
        name,
        validate,
        required,
        onValidationError,
        onChange,
        onBlur,
    });
    let handleBlur = useCallback((event) => {
        if (inputRef.current === null || event.target === inputRef.current) {
            return;
        }
        let inputs = Array.from(event.currentTarget.querySelectorAll("input,select"));
        let last = inputs[inputs.length - 1];
        if (last !== event.target) {
            return;
        }
        let issues = validationHandlers.validate(inputRef.current);
    }, []);
    return (
    // biome-ignore lint/a11y/noStaticElementInteractions: <explanation>
    <div {...props} ref={wrapper} className={styles.input} onBlur={handleBlur}>
			{/**
         * We have to use display: none and aria-hidden instead of type=hidden,
         * otherwise `dispatchEvent` wont trigger.
         */}
			<input id={id} aria-hidden style={{
            inlineSize: 0,
            blockSize: 0,
            insetBlockEnd: 0,
            insetInlineStart: 0,
            opacity: 0,
            position: "absolute",
        }} ref={hiddenInputRef} name={name} value={currentValue} onChange={validationHandlers.onChange} onBlur={validationHandlers.onBlur} onFocus={handleFocus} type="date"/>

			<DateInputCtx.Provider value={ctx}>{children}</DateInputCtx.Provider>
		</div>);
}
let createDaySchema = (numberOfDays) => pipe(optional(string()), transform((number) => Number.parseInt(number ?? "", 10)), minValue(1), maxValue(numberOfDays));
/**
 * A specialized input for entering the day of the month.
 *
 * @remarks
 * Validates that the entered day is valid for the currently selected month and year.
 *
 * @param props - Standard input props (excluding value/onChange).
 * @returns The rendered day input.
 */
export function Day({ ...props }) {
    let inputRef = useRef(null);
    let { day, setDay, month, year } = useDateInput();
    let schema = useMemo(() => {
        let numberOfDays = getNumberOfDays(year, month);
        return createDaySchema(numberOfDays);
    }, [year, month]);
    // biome-ignore lint/correctness/useExhaustiveDependencies: we want to revalidate when day changes
    useEffect(() => {
        if (inputRef.current === null) {
            return;
        }
        validateElement(inputRef.current, schema, props.required);
    }, [day, schema, props.required]);
    let handleChange = (e) => {
        let val = Number.parseInt(e.target.value, 10);
        if (Number.isNaN(val)) {
            setDay(null);
            return;
        }
        setDay(val);
    };
    return (<Input ref={inputRef} value={day ?? ""} onChange={handleChange} type="text" inputMode="numeric" pattern="(0?[1-9]|[12]\d|3[01])" validate={schema} {...props}/>);
}
/**
 * A select component for choosing the month.
 *
 * @param props - Standard select props (excluding value/required/onChange).
 * @returns The rendered month select.
 */
export function MonthSelect(props) {
    let months = useMonths();
    let { month: selectedMonth, setMonth } = useDateInput();
    let handleChange = (e) => {
        let val = Number.parseInt(e.target.value, 10);
        if (Number.isNaN(val)) {
            return;
        }
        setMonth(val);
    };
    return (<Select required value={selectedMonth ?? ""} onChange={handleChange} {...props}>
			{months.map((month) => (<Option key={month.label} value={month.value}>
					{month.label}
				</Option>))}
		</Select>);
}
let MonthSchema = pipe(optional(string()), transform((number) => Number.parseInt(number ?? "", 10)), minValue(1), maxValue(12));
/**
 * A numeric input for entering the month (1-12).
 *
 * @param props - Standard input props (excluding value/onChange).
 * @returns The rendered month input.
 */
export let MonthInput = (props) => {
    let inputRef = useRef(null);
    let { month, setMonth } = useDateInput();
    useEffect(() => {
        if (inputRef.current === null) {
            return;
        }
        validateElement(inputRef.current, MonthSchema, props.required);
    }, [props.required]);
    let handleChange = (e) => {
        let val = Number.parseInt(e.target.value, 10);
        if (Number.isNaN(val)) {
            setMonth(null);
            return;
        }
        setMonth(val - 1);
    };
    return (<Input value={month !== null ? month + 1 : ""} required onChange={handleChange} type="text" inputMode="numeric" pattern="(0?[1-9]|1[0-2])" validate={MonthSchema} {...props}/>);
};
/**
 * A numeric input for entering the year.
 *
 * @param props - Standard input props (excluding value/onChange).
 * @returns The rendered year input.
 */
export function Year(props) {
    let { year, setYear } = useDateInput();
    let handleChange = (e) => {
        let val = Number.parseInt(e.target.value, 10);
        if (Number.isNaN(val)) {
            setYear(null);
            return;
        }
        setYear(val);
    };
    return (<Input required value={year ?? ""} onChange={handleChange} type="number" {...props}/>);
}
/**
 * Calculates the number of days in a specific month of a year.
 *
 * @param year - The year.
 * @param month - The month (0-indexed).
 * @returns The number of days in the month.
 */
function getNumberOfDays(year, month) {
    if (month === null) {
        return 31;
    }
    let currentYear = year ?? new Date().getFullYear();
    return new Date(currentYear, month + 1, 0).getDate();
}
