/* eslint-disable @typescript-eslint/init-declarations */
export var Modifier;
(function (Modifier) {
    // The `*` modifier.
    Modifier[Modifier["kZeroOrMore"] = 0] = "kZeroOrMore";
    // The `?` modifier.
    Modifier[Modifier["kOptional"] = 1] = "kOptional";
    // The `+` modifier.
    Modifier[Modifier["kOneOrMore"] = 2] = "kOneOrMore";
    // No modifier.
    Modifier[Modifier["kNone"] = 3] = "kNone";
})(Modifier || (Modifier = {}));
export var PartType;
(function (PartType) {
    // A part that matches any character to the end of the input string.
    PartType[PartType["kFullWildcard"] = 0] = "kFullWildcard";
    // A part that matches any character to the next segment separator.
    PartType[PartType["kSegmentWildcard"] = 1] = "kSegmentWildcard";
    // A part with a custom regular expression.
    PartType[PartType["kRegex"] = 2] = "kRegex";
    // A fixed, non-variable part of the pattern.  Consists of kChar and
    // KEscapedChar Tokens.
    PartType[PartType["kFixed"] = 3] = "kFixed";
})(PartType || (PartType = {}));
export class Part {
    type = PartType.kFixed;
    name = '';
    prefix = '';
    value = '';
    suffix = '';
    modifier = Modifier.kNone;
    constructor(type, name, prefix, value, suffix, modifier) {
        this.type = type;
        this.name = name;
        this.prefix = prefix;
        this.value = value;
        this.suffix = suffix;
        this.modifier = modifier;
    }
    hasCustomName() {
        return this.name !== '' && typeof this.name !== 'number';
    }
}
// Note, the `//u` suffix triggers this typescript linting bug:
//
//  https://github.com/buzinas/tslint-eslint-rules/issues/289
//
// This requires disabling the no-empty-character-class lint rule.
const regexIdentifierStart = /[$_\p{ID_Start}]/u;
const regexIdentifierPart = /[$_\u200C\u200D\p{ID_Continue}]/u;
const kFullWildcardRegex = '.*';
function isASCII(str, extended) {
    return (extended ? /^[\x00-\xFF]*$/ : /^[\x00-\x7F]*$/).test(str);
}
/**
 * Tokenize input string.
 */
export function lexer(str, lenient = false) {
    const tokens = [];
    let i = 0;
    while (i < str.length) {
        const char = str[i];
        const ErrorOrInvalid = function (msg) {
            if (!lenient) {
                throw new TypeError(msg);
            }
            tokens.push({
                type: 'INVALID_CHAR',
                index: i,
                // @ts-ignore
                value: str[i++]
            });
        };
        if (char === '*') {
            tokens.push({
                type: 'ASTERISK',
                index: i,
                // @ts-ignore
                value: str[i++]
            });
            continue;
        }
        if (char === '+' || char === '?') {
            tokens.push({
                type: 'OTHER_MODIFIER',
                index: i,
                // @ts-ignore
                value: str[i++]
            });
            continue;
        }
        if (char === '\\') {
            tokens.push({
                type: 'ESCAPED_CHAR',
                index: i++,
                // @ts-ignore
                value: str[i++]
            });
            continue;
        }
        if (char === '{') {
            tokens.push({
                type: 'OPEN',
                index: i,
                // @ts-ignore
                value: str[i++]
            });
            continue;
        }
        if (char === '}') {
            tokens.push({
                type: 'CLOSE',
                index: i,
                // @ts-ignore
                value: str[i++]
            });
            continue;
        }
        if (char === ':') {
            let name = '';
            let j = i + 1;
            while (j < str.length) {
                const code = str[j];
                if ((j === i + 1 && regexIdentifierStart.test(code)) ||
                    (j !== i + 1 && regexIdentifierPart.test(code))) {
                    name += str[j++];
                    continue;
                }
                break;
            }
            if (!name) {
                ErrorOrInvalid(`Missing parameter name at ${i}`);
                continue;
            }
            tokens.push({
                type: 'NAME',
                index: i,
                value: name
            });
            i = j;
            continue;
        }
        if (char === '(') {
            let count = 1;
            let pattern = '';
            let j = i + 1;
            let error = false;
            if (str[j] === '?') {
                ErrorOrInvalid(`Pattern cannot start with "?" at ${j}`);
                continue;
            }
            while (j < str.length) {
                // @ts-ignore
                if (!isASCII(str[j], false)) {
                    ErrorOrInvalid(`Invalid character '${str[j]}' at ${j}.`);
                    error = true;
                    break;
                }
                if (str[j] === '\\') {
                    // @ts-ignore
                    pattern += str[j++] + str[j++];
                    continue;
                }
                if (str[j] === ')') {
                    count--;
                    if (count === 0) {
                        j++;
                        break;
                    }
                }
                else if (str[j] === '(') {
                    count++;
                    if (str[j + 1] !== '?') {
                        ErrorOrInvalid(`Capturing groups are not allowed at ${j}`);
                        error = true;
                        break;
                    }
                }
                pattern += str[j++];
            }
            if (error) {
                continue;
            }
            if (count) {
                ErrorOrInvalid(`Unbalanced pattern at ${i}`);
                continue;
            }
            if (!pattern) {
                ErrorOrInvalid(`Missing pattern at ${i}`);
                continue;
            }
            tokens.push({
                type: 'REGEX',
                index: i,
                value: pattern
            });
            i = j;
            continue;
        }
        tokens.push({
            type: 'CHAR',
            index: i,
            // @ts-ignore
            value: str[i++]
        });
    }
    tokens.push({
        type: 'END',
        index: i,
        value: ''
    });
    return tokens;
}
/**
 * Parse a string for the raw tokens.
 */
export function parse(str, options = {}) {
    const tokens = lexer(str);
    options.delimiter ??= '/#?';
    options.prefixes ??= './';
    const segmentWildcardRegex = `[^${escapeString(options.delimiter)}]+?`;
    const result = [];
    let key = 0;
    let i = 0;
    const path = '';
    const nameSet = new Set();
    const tryConsume = (type) => {
        // @ts-ignore
        if (i < tokens.length && tokens[i].type === type) {
            // @ts-ignore
            return tokens[i++].value;
        }
        return undefined;
    };
    const tryConsumeModifier = () => tryConsume('OTHER_MODIFIER') ?? tryConsume('ASTERISK');
    const mustConsume = (type) => {
        const value = tryConsume(type);
        if (value !== undefined) {
            return value;
        }
        // @ts-ignore
        const { type: nextType, index } = tokens[i];
        throw new TypeError(`Unexpected ${nextType} at ${index}, expected ${type}`);
    };
    const consumeText = () => {
        let result = '';
        let value;
        while (value = tryConsume('CHAR') ?? tryConsume('ESCAPED_CHAR')) {
            result += value;
        }
        return result;
    };
    const DefaultEncodePart = (value) => value;
    const encodePart = options.encodePart || DefaultEncodePart;
    let pendingFixedValue = '';
    const appendToPendingFixedValue = (value) => {
        pendingFixedValue += value;
    };
    const maybeAddPartFromPendingFixedValue = () => {
        if (!pendingFixedValue.length) {
            return;
        }
        result.push(new Part(PartType.kFixed, '', '', encodePart(pendingFixedValue), '', Modifier.kNone));
        pendingFixedValue = '';
    };
    const addPart = (prefix, nameToken, regexOrWildcardToken, suffix, modifierToken) => {
        let modifier = Modifier.kNone;
        switch (modifierToken) {
            case '?':
                modifier = Modifier.kOptional;
                break;
            case '*':
                modifier = Modifier.kZeroOrMore;
                break;
            case '+':
                modifier = Modifier.kOneOrMore;
                break;
        }
        // If this is a `{ ... }` grouping containing only fixed text, then
        // Just add it to our pending value for now.  We want to collect as
        // Much fixed text as possible in the buffer before commiting it to
        // A fixed part.
        if (!nameToken && !regexOrWildcardToken && modifier === Modifier.kNone) {
            appendToPendingFixedValue(prefix);
            return;
        }
        // We are about to add some kind of matching group Part to the list.
        // Before doing that make sure to flush any pending fixed test to a
        // KFixed Part.
        maybeAddPartFromPendingFixedValue();
        // If there is no name, regex, or wildcard tokens then this is just a fixed
        // String grouping; e.g. "{foo}?".  The fixed string ends up in the prefix
        // Value since it consumed the entire text of the grouping.  If the prefix
        // Value is empty then its an empty "{}" group and we return without adding
        // Any Part.
        if (!nameToken && !regexOrWildcardToken) {
            if (!prefix) {
                return;
            }
            result.push(new Part(PartType.kFixed, '', '', encodePart(prefix), '', modifier));
            return;
        }
        // Determine the regex value.  If there is a |kRegex| Token, then this is
        // Explicitly set by that Token.  If there is a wildcard token, then this
        // Is set to the |kFullWildcardRegex| constant.  Otherwise a kName Token by
        // Itself gets an implicit regex value that matches through to the end of
        // The segment. This is represented by the |regexOrWildcardToken| value.
        let regexValue;
        if (!regexOrWildcardToken) {
            regexValue = segmentWildcardRegex;
        }
        else if (regexOrWildcardToken === '*') {
            regexValue = kFullWildcardRegex;
        }
        else {
            regexValue = regexOrWildcardToken;
        }
        // Next determine the type of the Part.  This depends on the regex value
        // Since we give certain values special treatment with their own type.
        // A |segmentWildcardRegex| is mapped to the kSegmentWildcard type.  A
        // |kFullWildcardRegex| is mapped to the kFullWildcard type.  Otherwise
        // The Part gets the kRegex type.
        let type = PartType.kRegex;
        if (regexValue === segmentWildcardRegex) {
            type = PartType.kSegmentWildcard;
            regexValue = '';
        }
        else if (regexValue === kFullWildcardRegex) {
            type = PartType.kFullWildcard;
            regexValue = '';
        }
        // Every kRegex, kSegmentWildcard, and kFullWildcard Part must have a
        // Group name.  If there was a kName Token, then use the explicitly
        // Set name.  Otherwise we generate a numeric based key for the name.
        let name;
        if (nameToken) {
            name = nameToken;
        }
        else if (regexOrWildcardToken) {
            name = key++;
        }
        if (nameSet.has(name)) {
            throw new TypeError(`Duplicate name '${name}'.`);
        }
        nameSet.add(name);
        // @ts-ignore
        result.push(new Part(type, name, encodePart(prefix), regexValue, encodePart(suffix), modifier));
    };
    while (i < tokens.length) {
        // Look for the sequence: <prefix char><name><regex><modifier>
        // There could be from zero to all through of these tokens.  For
        // Example:
        //  * "/:foo(bar)?" - all four tokens
        //  * "/" - just a char token
        //  * ":foo" - just a name token
        //  * "(bar)" - just a regex token
        //  * "/:foo" - char and name tokens
        //  * "/(bar)" - char and regex tokens
        //  * "/:foo?" - char, name, and modifier tokens
        //  * "/(bar)?" - char, regex, and modifier tokens
        const charToken = tryConsume('CHAR');
        const nameToken = tryConsume('NAME');
        let regexOrWildcardToken = tryConsume('REGEX');
        // If there is no name or regex token, then we may have a wildcard `*`
        // Token in place of an unnamed regex token.  Each wildcard will be
        // Treated as being equivalent to a "(.*)" regex token.  For example:
        //  * "/*" - equivalent to "/(.*)"
        //  * "/*?" - equivalent to "/(.*)?"
        if (!nameToken && !regexOrWildcardToken) {
            regexOrWildcardToken = tryConsume('ASTERISK');
        }
        // If there is a name, regex, or wildcard token then we need to add a
        // Pattern Part immediately.
        if (nameToken || regexOrWildcardToken) {
            // Determine if the char token is a valid prefix.  Only characters in the
            // Configured prefix_list are automatically treated as prefixes.  A
            // KEscapedChar Token is never treated as a prefix.
            let prefix = charToken ?? '';
            if (!options.prefixes.includes(prefix)) {
                // This is not a prefix character.  Add it to the buffered characters
                // To be added as a kFixed Part later.
                appendToPendingFixedValue(prefix);
                prefix = '';
            }
            // If we have any buffered characters in a pending fixed value, then
            // Convert them into a kFixed Part now.
            maybeAddPartFromPendingFixedValue();
            // KName and kRegex tokens can optionally be followed by a modifier.
            const modifierToken = tryConsumeModifier();
            // Add the Part for the name and regex/wildcard tokens.
            // @ts-ignore
            addPart(prefix, nameToken, regexOrWildcardToken, '', modifierToken);
            continue;
        }
        // There was neither a kRegex or kName token, so consider if we just have a
        // Fixed string part.  A fixed string can consist of kChar or kEscapedChar
        // Tokens.  These just get added to the buffered pending fixed value for
        // Now. It will get converted to a kFixed Part later.
        const value = charToken ?? tryConsume('ESCAPED_CHAR');
        if (value) {
            appendToPendingFixedValue(value);
            continue;
        }
        // There was not a char or escaped char token, so we no we are at the end
        // Of any fixed string.  Do not yet convert the pending fixed value into
        // A kFixedPart, though.  Its possible there will be further fixed text in
        // A `{ ... }` group, etc.
        // Look for the sequence:
        //
        //  <open><char prefix><name><regex><char suffix><close><modifier>
        //
        // The open and close are required, but the other tokens are optional.
        // For example:
        //  * "{a:foo(.*)b}?" - all tokens present
        //  * "{:foo}?" - just name and modifier tokens
        //  * "{(.*)}?" - just regex and modifier tokens
        //  * "{ab}?" - just char and modifier tokens
        const openToken = tryConsume('OPEN');
        if (openToken) {
            const prefix = consumeText();
            const nameToken = tryConsume('NAME');
            let regexOrWildcardToken = tryConsume('REGEX');
            // If there is no name or regex token, then we may have a wildcard `*`
            // Token in place of an unnamed regex token.  Each wildcard will be
            // Treated as being equivalent to a "(.*)" regex token.  For example,
            // "{a*b}" is equivalent to "{a(.*)b}".
            if (!nameToken && !regexOrWildcardToken) {
                regexOrWildcardToken = tryConsume('ASTERISK');
            }
            const suffix = consumeText();
            mustConsume('CLOSE');
            const modifierToken = tryConsumeModifier();
            // @ts-ignore
            addPart(prefix, nameToken, regexOrWildcardToken, suffix, modifierToken);
            continue;
        }
        // We are about to end the pattern string, so flush any pending text to
        // A kFixed Part.
        maybeAddPartFromPendingFixedValue();
        // We didn't find any tokens allowed by the syntax, so we should be
        // At the end of the token list.  If there is a syntax error, this
        // Is where it will typically be caught.
        mustConsume('END');
    }
    return result;
}
/**
 * Escape a regular expression string.
 */
function escapeString(str) {
    return str.replace(/([.+*?^${}()[\]|/\\])/g, '\\$1');
}
/**
 * Get the flags for a regexp from the options.
 */
function flags(options) {
    return options?.ignoreCase ? 'ui' : 'u';
}
/**
 * Create a path regexp from string input.
 */
export function stringToRegexp(path, names, options) {
    return partsToRegexp(parse(path, options), names, options);
}
export function modifierToString(modifier) {
    switch (modifier) {
        case Modifier.kZeroOrMore:
            return '*';
        case Modifier.kOptional:
            return '?';
        case Modifier.kOneOrMore:
            return '+';
        case Modifier.kNone:
            return '';
    }
}
/**
 * Expose a function for taking tokens and returning a RegExp.
 */
// @ts-ignore
export function partsToRegexp(parts, names, options = {}) {
    options.delimiter ??= '/#?';
    // @ts-ignore
    options.prefixes ??= './';
    // @ts-ignore
    options.sensitive ??= false;
    options.strict ??= false;
    options.end ??= true;
    options.start ??= true;
    options.endsWith = '';
    let result = options.start ? '^' : '';
    // Iterate over the parts and create our regexp string.
    for (const part of parts) {
        // Handle kFixed Parts.  If there is a modifier we must wrap the escaped
        // Value in a non-capturing group.  Otherwise we just append the escaped
        // Value.  For example:
        //
        //  <escaped-fixed-value>
        //
        // Or:
        //
        //  (?:<escaped-fixed-value>)<modifier>
        //
        if (part.type === PartType.kFixed) {
            if (part.modifier === Modifier.kNone) {
                result += escapeString(part.value);
            }
            else {
                result += `(?:${escapeString(part.value)})${modifierToString(part.modifier)}`;
            }
            continue;
        }
        // All remaining Part types must have a name.  Append it to the output
        // Names if provided.
        if (names) {
            names.push(part.name);
        }
        const segmentWildcardRegex = `[^${escapeString(options.delimiter)}]+?`;
        // Compute the Part regex value.  For kSegmentWildcard and kFullWildcard
        // Types we must convert the type enum back to the defined regex value.
        let regexValue = part.value;
        if (part.type === PartType.kSegmentWildcard) {
            regexValue = segmentWildcardRegex;
        }
        else if (part.type === PartType.kFullWildcard) {
            regexValue = kFullWildcardRegex;
        }
        // Handle the case where there is no prefix or suffix value.  This varies a
        // Bit depending on the modifier.
        //
        // If there is no modifier or an optional modifier, then we simply wrap the
        // Regex value in a capturing group:
        //
        //  (<regex-value>)<modifier>
        //
        // If there is a modifier, then we need to use a non-capturing group for the
        // Regex value and an outer capturing group that includes the modifier as
        // Well.  Like:
        //
        //  ((?:<regex-value>)<modifier>)
        if (!part.prefix.length && !part.suffix.length) {
            if (part.modifier === Modifier.kNone ||
                part.modifier === Modifier.kOptional) {
                result += `(${regexValue})${modifierToString(part.modifier)}`;
            }
            else {
                result += `((?:${regexValue})${modifierToString(part.modifier)})`;
            }
            continue;
        }
        // Handle non-repeating regex Parts with a prefix and/or suffix.  The
        // Capturing group again only contains the regex value.  This inner group
        // Is compined with the prefix and/or suffix in an outer non-capturing
        // Group.  Finally the modifier is applied to the entire outer group.
        // For example:
        //
        //  (?:<prefix>(<regex-value>)<suffix>)<modifier>
        //
        if (part.modifier === Modifier.kNone ||
            part.modifier === Modifier.kOptional) {
            result += `(?:${escapeString(part.prefix)}(${regexValue})${escapeString(part.suffix)})`;
            result += modifierToString(part.modifier);
            continue;
        }
        // Repeating Parts are dramatically more complicated.  We want to exclude
        // The initial prefix and the final suffix, but include them between any
        // Repeated elements.  To achieve this we provide a separate initial
        // Part that excludes the prefix.  Then the part is duplicated with the
        // Prefix/suffix values included in an optional repeating element.  If
        // Zero values are permitted then a final optional modifier may be added.
        // For example:
        //
        //  (?:<prefix>((?:<regex-value>)(?:<suffix><prefix>(?:<regex-value>))*)<suffix>)?
        //
        result += `(?:${escapeString(part.prefix)}`;
        result += `((?:${regexValue})(?:`;
        result += escapeString(part.suffix);
        result += escapeString(part.prefix);
        result += `(?:${regexValue}))*)${escapeString(part.suffix)})`;
        if (part.modifier === Modifier.kZeroOrMore) {
            result += '?';
        }
    }
    const endsWith = `[${escapeString(options.endsWith)}]|$`;
    const delimiter = `[${escapeString(options.delimiter)}]`;
    // Should we anchor the pattern to the end of the input string?
    if (options.end) {
        // In non-strict mode an optional delimiter character is always
        // Permitted at the end of the string.  For example, if the pattern
        // Is "/foo/bar" then it would match "/foo/bar/".
        //
        //  [<delimiter chars>]?
        //
        if (!options.strict) {
            result += `${delimiter}?`;
        }
        // The options ends_with value contains a list of characters that
        // May also signal the end of the pattern match.
        if (!options.endsWith.length) {
            // Simply anchor to the end of the input string.
            result += '$';
        }
        else {
            // Anchor to either a ends_with character or the end of the input
            // String.  This uses a lookahead assertion.
            //
            //  (?=[<ends_with chars>]|$)
            //
            result += `(?=${endsWith})`;
        }
        return new RegExp(result, flags(options));
    }
    // We are not anchored to the end of the input string.
    // Again, if not in strict mode we permit an optional trailing delimiter
    // Character before anchoring to any ends_with characters with a lookahead
    // Assertion.
    //
    //  (?:[<delimiter chars>](?=[<ends_with chars>]|$))?
    if (!options.strict) {
        result += `(?:${delimiter}(?=${endsWith}))?`;
    }
    // Further, if the pattern does not end with a trailing delimiter character
    // We also anchor to a delimiter character in our lookahead assertion.  So
    // A pattern "/foo/bar" would match "/foo/bar/baz", but not "/foo/barbaz".
    //
    //  (?=[<delimiter chars>]|[<ends_with chars>]|$)
    let isEndDelimited = false;
    if (parts.length) {
        const lastPart = parts[parts.length - 1];
        // @ts-ignore
        if (lastPart.type === PartType.kFixed && lastPart.modifier === Modifier.kNone) {
            // @ts-ignore
            isEndDelimited = options.delimiter.includes(lastPart);
        }
    }
    if (!isEndDelimited) {
        result += `(?=${delimiter}|${endsWith})`;
    }
    return new RegExp(result, flags(options));
}
//# sourceMappingURL=PathToRegexp.js.map