/**
 * A utility class for parsing date-time strings using format strings.
 *
 * Supported format specifiers:
 * - `d`   : Day of the month (1-31) without leading zero.
 * - `dd`  : Day of the month (01-31) with leading zero.
 * - `M`   : Month (1-12) without leading zero.
 * - `MM`  : Month (01-12) with leading zero.
 * - `h`   : Hour (1-12) in 12-hour format without leading zero.
 * - `hh`  : Hour (01-12) in 12-hour format with leading zero.
 * - `H`   : Hour (0-23) in 24-hour format without leading zero.
 * - `HH`  : Hour (00-23) in 24-hour format with leading zero.
 * - `m`   : Minute (0-59) without leading zero.
 * - `mm`  : Minute (00-59) with leading zero.
 * - `s`   : Second (0-59) without leading zero.
 * - `ss`  : Second (00-59) with leading zero.
 * - `tt`  : AM/PM designator.
 * - `y`   : Year as the last two digits without leading zero.
 * - `yy`  : Year as the last two digits with leading zero.
 * - `yyyy`: Year as a four-digit number.
 * - `:`   : Time separator.
 * - `/`   : Date separator.
 * - `-`   : Separator (hyphen).
 * - `.`   : Separator (dot).
 * - ` `   : Separator (space).
 *
 * @public
 */
export class DateTimeParser {
    // #region Fields
    static _segmentRegex = /d{1,2}|y{1,4}|M{1,2}|H{1,2}|h{1,2}|m{1,2}|s{1,2}|t{1,2}|[-:/. ]+/g;
    // #endregion
    // #region Methods
    /**
     * Attempts to parse a date-time string using the provided format.
     * Returns `null` if parsing fails or produces an invalid date.
     *
     * @param value The date-time string to parse.
     * @param format The expected format of the date-time string.
     * @param locale The locale (reserved for future use).
     * @returns The parsed Date or null.
     */
    static tryParse(value, format, locale) {
        try {
            const date = this.parse(value, format, locale);
            if (!this.isValidDate(date)) {
                return null;
            }
            return date;
        }
        catch {
            return null;
        }
    }
    /**
     * Parses a date-time string according to the given format string.
     *
     * @param value The date-time string to parse.
     * @param format The format string describing the layout.
     * @param locale The locale (reserved for future use).
     * @returns The parsed Date.
     * @throws Error if the format or value cannot be parsed.
     */
    static parse(value, format, locale) {
        const segments = format.match(this._segmentRegex);
        if (!segments) {
            throw new Error(`Unable to parse format string: "${format}".`);
        }
        let position = 0;
        let day = 1;
        let month = 0;
        let year = 0;
        let hours = 0;
        let minutes = 0;
        let seconds = 0;
        let isPm = null;
        let is12Hour = false;
        for (const segment of segments) {
            if ((/[-:/. ]+/).test(segment)) {
                position += segment.length;
                continue;
            }
            if (segment === 't' || segment === 'tt') {
                const amPm = this.extractAmPm(value, position);
                isPm = amPm.isPm;
                position += amPm.length;
                continue;
            }
            const extracted = this.extractNumericValue(value, position, segment);
            position += extracted.length;
            switch (segment) {
                case 'd':
                case 'dd':
                    day = extracted.value;
                    break;
                case 'M':
                case 'MM':
                    month = extracted.value - 1;
                    break;
                case 'y':
                case 'yy':
                    year = extracted.value < 100 ? 2000 + extracted.value : extracted.value;
                    break;
                case 'yyy':
                case 'yyyy':
                    year = extracted.value;
                    break;
                case 'h':
                case 'hh':
                    hours = extracted.value;
                    is12Hour = true;
                    break;
                case 'H':
                case 'HH':
                    hours = extracted.value;
                    break;
                case 'm':
                case 'mm':
                    minutes = extracted.value;
                    break;
                case 's':
                case 'ss':
                    seconds = extracted.value;
                    break;
                default:
                    throw new Error(`Unknown datetime format segment: "${segment}".`);
            }
        }
        // Validate 12-hour range (must be 1-12)
        if (is12Hour && (hours < 1 || hours > 12)) {
            throw new Error(`Invalid 12-hour value: ${hours}. Must be between 1 and 12.`);
        }
        // Convert 12-hour to 24-hour
        if (is12Hour && isPm !== null) {
            if (isPm && hours !== 12) {
                hours += 12;
            }
            else if (!isPm && hours === 12) {
                hours = 0;
            }
        }
        const date = new Date(year, month, day, hours, minutes, seconds);
        // Validate that the constructed date matches the parsed values
        if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
            throw new Error(`Invalid date values: day=${day}, month=${month + 1}, year=${year}.`);
        }
        // Validate that the constructed time matches the parsed values
        if (date.getHours() !== hours || date.getMinutes() !== minutes || date.getSeconds() !== seconds) {
            throw new Error(`Invalid time values: hours=${hours}, minutes=${minutes}, seconds=${seconds}.`);
        }
        return date;
    }
    /**
     * Extracts a numeric value from the input string at the given position.
     * The number of digits consumed is determined by the format segment:
     * - `yyyy` / `yyy`: up to 4 digits.
     * - All other segments (`dd`, `MM`, `HH`, `hh`, `mm`, `ss`, etc.): up to 2 digits.
     * Single-character specifiers (`d`, `M`, `H`, `h`, `m`, `s`) require at least 1 digit;
     * two-character specifiers require exactly 2.
     *
     * @param value - The full input string being parsed.
     * @param position - The current character index in `value`.
     * @param segment - The format segment (e.g., `"HH"`, `"m"`, `"yyyy"`).
     * @returns An object containing the parsed numeric `value` and the `length` of characters consumed.
     * @throws Error if fewer digits are found than the segment requires.
     */
    static extractNumericValue(value, position, segment) {
        const maxLength = segment === 'yyyy' || segment === 'yyy' ? 4 : 2;
        const minLength = segment.length === 1 ? 1 : segment.length;
        let end = position;
        while (end < value.length && end - position < maxLength && (/\d/).test(value[end])) {
            end++;
        }
        const extracted = value.substring(position, end);
        if (extracted.length < minLength) {
            throw new Error(`Expected at least ${minLength} digit(s) for segment "${segment}" at position ${position}, got "${extracted}".`);
        }
        const numericValue = parseInt(extracted, 10);
        if (isNaN(numericValue)) {
            throw new Error(`Unable to parse numeric value for segment "${segment}" at position ${position}.`);
        }
        return { value: numericValue, length: extracted.length };
    }
    /**
     * Extracts the AM/PM designator from the input string at the given position.
     * Recognized patterns (case-insensitive): `"AM"`, `"PM"`, `"A.M."`, `"P.M."`,
     * and single characters `"A"` / `"P"` (for the `t` specifier).
     *
     * @param value - The full input string being parsed.
     * @param position - The current character index in `value`.
     * @returns An object with `isPm` (true for PM) and `length` of characters consumed.
     * @throws Error if no valid AM/PM designator is found at the position.
     */
    static extractAmPm(value, position) {
        const remaining = value.substring(position).toUpperCase();
        if (remaining.startsWith('PM') || remaining.startsWith('P.M.')) {
            const length = remaining.startsWith('P.M.') ? 4 : 2;
            return { isPm: true, length };
        }
        if (remaining.startsWith('AM') || remaining.startsWith('A.M.')) {
            const length = remaining.startsWith('A.M.') ? 4 : 2;
            return { isPm: false, length };
        }
        // Single character: 't' specifier
        if (remaining.startsWith('P')) {
            return { isPm: true, length: 1 };
        }
        if (remaining.startsWith('A')) {
            return { isPm: false, length: 1 };
        }
        throw new Error(`Unable to parse AM/PM designator at position ${position}.`);
    }
    /**
     * Checks whether the given Date object represents a valid point in time.
     *
     * @param date - The Date object to validate.
     * @returns `true` if the date is valid (not `NaN`), otherwise `false`.
     */
    static isValidDate(date) {
        return !isNaN(date.getTime());
    }
}
//# sourceMappingURL=DateTimeParser.js.map