import { createMulticharIndex, createRegularIndex, emptyMulticharIndex, emptyRegularIndex } from './indexes.js';
import { calculatePseudoSignatures, defaultPseudoSignature, emptyPseudoSignatures } from './pseudo-signatures.js';
import { cssSyntaxDefinitions, extendSyntaxDefinition, getXmlOptions, cssModules, pseudoLocationIndex } from './syntax-definitions.js';
import { digitsChars, isHex, isIdent, isIdentStart, maxHexLength, quoteChars, whitespaceChars } from './utils.js';
var errorPrefix = "css-selector-parser parse error: ";
/**
 * Creates a parse function to be used later to parse CSS selectors.
 */
export function createParser(options) {
    if (options === void 0) { options = {}; }
    var _a = options.syntax, syntax = _a === void 0 ? 'latest' : _a, substitutes = options.substitutes, _b = options.strict, strict = _b === void 0 ? true : _b, modules = options.modules;
    var syntaxDefinition = typeof syntax === 'object' ? syntax : cssSyntaxDefinitions[syntax];
    if (syntaxDefinition.baseSyntax) {
        syntaxDefinition = extendSyntaxDefinition(cssSyntaxDefinitions[syntaxDefinition.baseSyntax], syntaxDefinition);
    }
    // Apply modules from syntax definition
    if (syntaxDefinition.modules && syntaxDefinition.modules.length > 0) {
        for (var _i = 0, _c = syntaxDefinition.modules; _i < _c.length; _i++) {
            var module_1 = _c[_i];
            var moduleSyntax = cssModules[module_1];
            if (moduleSyntax) {
                syntaxDefinition = extendSyntaxDefinition(moduleSyntax, syntaxDefinition);
            }
        }
    }
    // Apply additional modules if specified from options
    if (modules && modules.length > 0) {
        for (var _d = 0, modules_1 = modules; _d < modules_1.length; _d++) {
            var module_2 = modules_1[_d];
            var moduleSyntax = cssModules[module_2];
            if (moduleSyntax) {
                syntaxDefinition = extendSyntaxDefinition(moduleSyntax, syntaxDefinition);
            }
        }
    }
    var _e = syntaxDefinition.tag
        ? [true, Boolean(getXmlOptions(syntaxDefinition.tag).wildcard)]
        : [false, false], tagNameEnabled = _e[0], tagNameWildcardEnabled = _e[1];
    var idEnabled = Boolean(syntaxDefinition.ids);
    var classNamesEnabled = Boolean(syntaxDefinition.classNames);
    var namespaceEnabled = Boolean(syntaxDefinition.namespace);
    var namespaceWildcardEnabled = syntaxDefinition.namespace &&
        (syntaxDefinition.namespace === true || syntaxDefinition.namespace.wildcard === true);
    if (namespaceEnabled && !tagNameEnabled) {
        throw new Error("".concat(errorPrefix, "Namespaces cannot be enabled while tags are disabled."));
    }
    var substitutesEnabled = Boolean(substitutes);
    var combinatorsIndex = syntaxDefinition.combinators
        ? createMulticharIndex(syntaxDefinition.combinators)
        : emptyMulticharIndex;
    var _f = syntaxDefinition.attributes
        ? [
            true,
            syntaxDefinition.attributes.operators
                ? createMulticharIndex(syntaxDefinition.attributes.operators)
                : emptyMulticharIndex,
            syntaxDefinition.attributes.caseSensitivityModifiers
                ? createRegularIndex(syntaxDefinition.attributes.caseSensitivityModifiers)
                : emptyRegularIndex,
            syntaxDefinition.attributes.unknownCaseSensitivityModifiers === 'accept'
        ]
        : [false, emptyMulticharIndex, emptyRegularIndex, false], attributesEnabled = _f[0], attributesOperatorsIndex = _f[1], attributesCaseSensitivityModifiers = _f[2], attributesAcceptUnknownCaseSensitivityModifiers = _f[3];
    var attributesCaseSensitivityModifiersEnabled = attributesAcceptUnknownCaseSensitivityModifiers || Object.keys(attributesCaseSensitivityModifiers).length > 0;
    var _g = syntaxDefinition.pseudoClasses
        ? [
            true,
            syntaxDefinition.pseudoClasses.definitions
                ? calculatePseudoSignatures(syntaxDefinition.pseudoClasses.definitions)
                : emptyPseudoSignatures,
            syntaxDefinition.pseudoClasses.unknown === 'accept'
        ]
        : [false, emptyPseudoSignatures, false], pseudoClassesEnabled = _g[0], pseudoClassesDefinitions = _g[1], pseudoClassesAcceptUnknown = _g[2];
    var _h = syntaxDefinition.pseudoElements
        ? [
            true,
            syntaxDefinition.pseudoElements.notation === 'singleColon' ||
                syntaxDefinition.pseudoElements.notation === 'both',
            !syntaxDefinition.pseudoElements.notation ||
                syntaxDefinition.pseudoElements.notation === 'doubleColon' ||
                syntaxDefinition.pseudoElements.notation === 'both',
            syntaxDefinition.pseudoElements.definitions
                ? calculatePseudoSignatures(Array.isArray(syntaxDefinition.pseudoElements.definitions)
                    ? { NoArgument: syntaxDefinition.pseudoElements.definitions }
                    : syntaxDefinition.pseudoElements.definitions)
                : emptyPseudoSignatures,
            syntaxDefinition.pseudoElements.unknown === 'accept'
        ]
        : [false, false, false, emptyPseudoSignatures, false], pseudoElementsEnabled = _h[0], pseudoElementsSingleColonNotationEnabled = _h[1], pseudoElementsDoubleColonNotationEnabled = _h[2], pseudoElementsDefinitions = _h[3], pseudoElementsAcceptUnknown = _h[4];
    var str = '';
    var l = str.length;
    var pos = 0;
    var chr = '';
    var is = function (comparison) { return chr === comparison; };
    var isTagStart = function () { return is('*') || isIdentStart(chr); };
    var rewind = function (newPos) {
        pos = newPos;
        chr = str.charAt(pos);
    };
    var next = function () {
        pos++;
        chr = str.charAt(pos);
    };
    var readAndNext = function () {
        var current = chr;
        pos++;
        chr = str.charAt(pos);
        return current;
    };
    /** @throws ParserError */
    function fail(errorMessage) {
        var position = Math.min(l - 1, pos);
        var error = new Error("".concat(errorPrefix).concat(errorMessage, " Pos: ").concat(position, "."));
        error.position = position;
        error.name = 'ParserError';
        throw error;
    }
    function assert(condition, errorMessage) {
        if (!condition) {
            return fail(errorMessage);
        }
    }
    var assertNonEof = function () {
        assert(pos < l, 'Unexpected end of input.');
    };
    var isEof = function () { return pos >= l; };
    var pass = function (character) {
        assert(pos < l, "Expected \"".concat(character, "\" but end of input reached."));
        assert(chr === character, "Expected \"".concat(character, "\" but \"").concat(chr, "\" found."));
        pos++;
        chr = str.charAt(pos);
    };
    function matchMulticharIndex(index) {
        var match = matchMulticharIndexPos(index, pos);
        if (match) {
            pos += match.length;
            chr = str.charAt(pos);
            return match;
        }
    }
    function matchMulticharIndexPos(index, subPos) {
        var char = str.charAt(subPos);
        var charIndex = index[char];
        if (charIndex) {
            var subMatch = matchMulticharIndexPos(charIndex.chars, subPos + 1);
            if (subMatch) {
                return subMatch;
            }
            if (charIndex.self) {
                return charIndex.self;
            }
        }
    }
    /**
     * @see https://www.w3.org/TR/css-syntax/#hex-digit-diagram
     */
    function parseHex() {
        var hex = readAndNext();
        var count = 1;
        while (isHex(chr) && count < maxHexLength) {
            hex += readAndNext();
            count++;
        }
        skipSingleWhitespace();
        return String.fromCharCode(parseInt(hex, 16));
    }
    /**
     * @see https://www.w3.org/TR/css-syntax/#string-token-diagram
     */
    function parseString(quote) {
        var result = '';
        pass(quote);
        while (pos < l) {
            if (is(quote)) {
                next();
                return result;
            }
            else if (is('\\')) {
                next();
                if (is(quote)) {
                    result += quote;
                    next();
                }
                else if (chr === '\n' || chr === '\f') {
                    next();
                }
                else if (chr === '\r') {
                    next();
                    if (is('\n')) {
                        next();
                    }
                }
                else if (isHex(chr)) {
                    result += parseHex();
                }
                else {
                    result += chr;
                    next();
                }
            }
            else {
                result += chr;
                next();
            }
        }
        return result;
    }
    /**
     * @see https://www.w3.org/TR/css-syntax/#ident-token-diagram
     */
    function parseIdentifier() {
        if (!isIdentStart(chr)) {
            return null;
        }
        var result = '';
        while (is('-')) {
            result += chr;
            next();
        }
        if (result === '-' && !isIdent(chr) && !is('\\')) {
            fail('Identifiers cannot consist of a single hyphen.');
        }
        if (strict && result.length >= 2) {
            // Checking this only for strict mode since browsers work fine with these identifiers.
            fail('Identifiers cannot start with two hyphens with strict mode on.');
        }
        if (digitsChars[chr]) {
            fail('Identifiers cannot start with hyphens followed by digits.');
        }
        while (pos < l) {
            if (isIdent(chr)) {
                result += readAndNext();
            }
            else if (is('\\')) {
                next();
                assertNonEof();
                if (isHex(chr)) {
                    result += parseHex();
                }
                else {
                    result += readAndNext();
                }
            }
            else {
                break;
            }
        }
        return result;
    }
    function parsePseudoClassString() {
        var result = '';
        while (pos < l) {
            if (is(')')) {
                break;
            }
            else if (is('\\')) {
                next();
                if (isEof() && !strict) {
                    return (result + '\\').trim();
                }
                assertNonEof();
                if (isHex(chr)) {
                    result += parseHex();
                }
                else {
                    result += readAndNext();
                }
            }
            else {
                result += readAndNext();
            }
        }
        return result.trim();
    }
    function skipSingleWhitespace() {
        if (chr === ' ' || chr === '\t' || chr === '\f' || chr === '\n') {
            next();
            return;
        }
        if (chr === '\r') {
            next();
        }
        if (chr === '\n') {
            next();
        }
    }
    function skipWhitespace() {
        while (whitespaceChars[chr]) {
            next();
        }
    }
    function parseSelector(relative) {
        if (relative === void 0) { relative = false; }
        skipWhitespace();
        var rules = [parseRule(relative)];
        while (is(',')) {
            next();
            skipWhitespace();
            rules.push(parseRule(relative));
        }
        return {
            type: 'Selector',
            rules: rules
        };
    }
    function parseAttribute() {
        pass('[');
        skipWhitespace();
        var attr;
        if (is('|')) {
            assert(namespaceEnabled, 'Namespaces are not enabled.');
            next();
            var name_1 = parseIdentifier();
            assert(name_1, 'Expected attribute name.');
            attr = {
                type: 'Attribute',
                name: name_1,
                namespace: { type: 'NoNamespace' }
            };
        }
        else if (is('*')) {
            assert(namespaceEnabled, 'Namespaces are not enabled.');
            assert(namespaceWildcardEnabled, 'Wildcard namespace is not enabled.');
            next();
            pass('|');
            var name_2 = parseIdentifier();
            assert(name_2, 'Expected attribute name.');
            attr = {
                type: 'Attribute',
                name: name_2,
                namespace: { type: 'WildcardNamespace' }
            };
        }
        else {
            var identifier = parseIdentifier();
            assert(identifier, 'Expected attribute name.');
            attr = {
                type: 'Attribute',
                name: identifier
            };
            if (is('|')) {
                var savedPos = pos;
                next();
                if (isIdentStart(chr)) {
                    assert(namespaceEnabled, 'Namespaces are not enabled.');
                    var name_3 = parseIdentifier();
                    assert(name_3, 'Expected attribute name.');
                    attr = {
                        type: 'Attribute',
                        name: name_3,
                        namespace: { type: 'NamespaceName', name: identifier }
                    };
                }
                else {
                    rewind(savedPos);
                }
            }
        }
        assert(attr.name, 'Expected attribute name.');
        skipWhitespace();
        if (isEof() && !strict) {
            return attr;
        }
        if (is(']')) {
            next();
        }
        else {
            attr.operator = matchMulticharIndex(attributesOperatorsIndex);
            assert(attr.operator, 'Expected a valid attribute selector operator.');
            skipWhitespace();
            assertNonEof();
            if (quoteChars[chr]) {
                attr.value = {
                    type: 'String',
                    value: parseString(chr)
                };
            }
            else if (substitutesEnabled && is('$')) {
                next();
                var name_4 = parseIdentifier();
                assert(name_4, 'Expected substitute name.');
                attr.value = {
                    type: 'Substitution',
                    name: name_4
                };
            }
            else {
                var value = parseIdentifier();
                assert(value, 'Expected attribute value.');
                attr.value = {
                    type: 'String',
                    value: value
                };
            }
            skipWhitespace();
            if (isEof() && !strict) {
                return attr;
            }
            if (!is(']')) {
                var caseSensitivityModifier = parseIdentifier();
                assert(caseSensitivityModifier, 'Expected end of attribute selector.');
                attr.caseSensitivityModifier = caseSensitivityModifier;
                assert(attributesCaseSensitivityModifiersEnabled, 'Attribute case sensitivity modifiers are not enabled.');
                assert(attributesAcceptUnknownCaseSensitivityModifiers ||
                    attributesCaseSensitivityModifiers[attr.caseSensitivityModifier], 'Unknown attribute case sensitivity modifier.');
                skipWhitespace();
                if (isEof() && !strict) {
                    return attr;
                }
            }
            pass(']');
        }
        return attr;
    }
    function parseNumber() {
        var result = '';
        while (digitsChars[chr]) {
            result += readAndNext();
        }
        assert(result !== '', 'Formula parse error.');
        return parseInt(result);
    }
    var isNumberStart = function () { return is('-') || is('+') || digitsChars[chr]; };
    function parseFormula() {
        if (is('e') || is('o')) {
            var ident = parseIdentifier();
            if (ident === 'even') {
                skipWhitespace();
                return [2, 0];
            }
            if (ident === 'odd') {
                skipWhitespace();
                return [2, 1];
            }
        }
        var firstNumber = null;
        var firstNumberMultiplier = 1;
        if (is('-')) {
            next();
            firstNumberMultiplier = -1;
        }
        if (isNumberStart()) {
            if (is('+')) {
                next();
            }
            firstNumber = parseNumber();
            if (!is('\\') && !is('n')) {
                return [0, firstNumber * firstNumberMultiplier];
            }
        }
        if (firstNumber === null) {
            firstNumber = 1;
        }
        firstNumber *= firstNumberMultiplier;
        var identifier;
        if (is('\\')) {
            next();
            if (isHex(chr)) {
                identifier = parseHex();
            }
            else {
                identifier = readAndNext();
            }
        }
        else {
            identifier = readAndNext();
        }
        assert(identifier === 'n', 'Formula parse error: expected "n".');
        skipWhitespace();
        if (is('+') || is('-')) {
            var sign = is('+') ? 1 : -1;
            next();
            skipWhitespace();
            return [firstNumber, sign * parseNumber()];
        }
        else {
            return [firstNumber, 0];
        }
    }
    function parsePseudoArgument(pseudoName, type, signature) {
        var argument;
        if (is('(')) {
            next();
            skipWhitespace();
            if (substitutesEnabled && is('$')) {
                next();
                var name_5 = parseIdentifier();
                assert(name_5, 'Expected substitute name.');
                argument = {
                    type: 'Substitution',
                    name: name_5
                };
            }
            else if (signature.type === 'String') {
                argument = {
                    type: 'String',
                    value: parsePseudoClassString()
                };
                assert(argument.value, "Expected ".concat(type, " argument value."));
            }
            else if (signature.type === 'Selector') {
                argument = parseSelector(true);
            }
            else if (signature.type === 'Formula') {
                var _a = parseFormula(), a = _a[0], b = _a[1];
                argument = {
                    type: 'Formula',
                    a: a,
                    b: b
                };
                if (signature.ofSelector) {
                    skipWhitespace();
                    if (is('o') || is('\\')) {
                        var ident = parseIdentifier();
                        assert(ident === 'of', 'Formula of selector parse error.');
                        skipWhitespace();
                        argument = {
                            type: 'FormulaOfSelector',
                            a: a,
                            b: b,
                            selector: parseRule()
                        };
                    }
                }
            }
            else {
                return fail("Invalid ".concat(type, " signature."));
            }
            skipWhitespace();
            if (isEof() && !strict) {
                return argument;
            }
            pass(')');
        }
        else {
            assert(signature.optional, "Argument is required for ".concat(type, " \"").concat(pseudoName, "\"."));
        }
        return argument;
    }
    function parseTagName() {
        if (is('*')) {
            assert(tagNameWildcardEnabled, 'Wildcard tag name is not enabled.');
            next();
            return { type: 'WildcardTag' };
        }
        else if (isIdentStart(chr)) {
            assert(tagNameEnabled, 'Tag names are not enabled.');
            var name_6 = parseIdentifier();
            assert(name_6, 'Expected tag name.');
            return {
                type: 'TagName',
                name: name_6
            };
        }
        else {
            return fail('Expected tag name.');
        }
    }
    function parseTagNameWithNamespace() {
        if (is('*')) {
            var savedPos = pos;
            next();
            if (!is('|')) {
                rewind(savedPos);
                return parseTagName();
            }
            next();
            if (!isTagStart()) {
                rewind(savedPos);
                return parseTagName();
            }
            assert(namespaceEnabled, 'Namespaces are not enabled.');
            assert(namespaceWildcardEnabled, 'Wildcard namespace is not enabled.');
            var tagName = parseTagName();
            tagName.namespace = { type: 'WildcardNamespace' };
            return tagName;
        }
        else if (is('|')) {
            assert(namespaceEnabled, 'Namespaces are not enabled.');
            next();
            var tagName = parseTagName();
            tagName.namespace = { type: 'NoNamespace' };
            return tagName;
        }
        else if (isIdentStart(chr)) {
            var identifier = parseIdentifier();
            assert(identifier, 'Expected tag name.');
            if (!is('|')) {
                assert(tagNameEnabled, 'Tag names are not enabled.');
                return {
                    type: 'TagName',
                    name: identifier
                };
            }
            var savedPos = pos;
            next();
            if (!isTagStart()) {
                rewind(savedPos);
                return {
                    type: 'TagName',
                    name: identifier
                };
            }
            assert(namespaceEnabled, 'Namespaces are not enabled.');
            var tagName = parseTagName();
            tagName.namespace = { type: 'NamespaceName', name: identifier };
            return tagName;
        }
        else {
            return fail('Expected tag name.');
        }
    }
    function parseRule(relative) {
        var _a, _b;
        if (relative === void 0) { relative = false; }
        var rule = { type: 'Rule', items: [] };
        if (relative) {
            var combinator = matchMulticharIndex(combinatorsIndex);
            if (combinator) {
                rule.combinator = combinator;
                skipWhitespace();
            }
        }
        while (pos < l) {
            if (isTagStart()) {
                assert(rule.items.length === 0, 'Unexpected tag/namespace start.');
                rule.items.push(parseTagNameWithNamespace());
            }
            else if (is('|')) {
                var savedPos = pos;
                next();
                if (isTagStart()) {
                    assert(rule.items.length === 0, 'Unexpected tag/namespace start.');
                    rewind(savedPos);
                    rule.items.push(parseTagNameWithNamespace());
                }
                else {
                    rewind(savedPos);
                    break;
                }
            }
            else if (is('.')) {
                assert(classNamesEnabled, 'Class names are not enabled.');
                next();
                var className = parseIdentifier();
                assert(className, 'Expected class name.');
                rule.items.push({ type: 'ClassName', name: className });
            }
            else if (is('#')) {
                assert(idEnabled, 'IDs are not enabled.');
                next();
                var idName = parseIdentifier();
                assert(idName, 'Expected ID name.');
                rule.items.push({ type: 'Id', name: idName });
            }
            else if (is('[')) {
                assert(attributesEnabled, 'Attributes are not enabled.');
                rule.items.push(parseAttribute());
            }
            else if (is(':')) {
                var isDoubleColon = false;
                var isPseudoElement = false;
                next();
                if (is(':')) {
                    assert(pseudoElementsEnabled, 'Pseudo elements are not enabled.');
                    assert(pseudoElementsDoubleColonNotationEnabled, 'Pseudo elements double colon notation is not enabled.');
                    isDoubleColon = true;
                    next();
                }
                var pseudoName = parseIdentifier();
                assert(isDoubleColon || pseudoName, 'Expected pseudo-class name.');
                assert(!isDoubleColon || pseudoName, 'Expected pseudo-element name.');
                assert(pseudoName, 'Expected pseudo-class name.');
                if (!isDoubleColon ||
                    pseudoElementsAcceptUnknown ||
                    Object.prototype.hasOwnProperty.call(pseudoElementsDefinitions, pseudoName)) {
                    // All good
                }
                else {
                    // Generate a helpful error message with location information
                    var locations = pseudoLocationIndex.pseudoElements[pseudoName];
                    var errorMessage = "Unknown pseudo-element \"".concat(pseudoName, "\"");
                    if (locations && locations.length > 0) {
                        errorMessage += ". It is defined in: ".concat(locations.join(', '));
                    }
                    fail(errorMessage + '.');
                }
                isPseudoElement =
                    pseudoElementsEnabled &&
                        (isDoubleColon ||
                            (!isDoubleColon &&
                                pseudoElementsSingleColonNotationEnabled &&
                                Object.prototype.hasOwnProperty.call(pseudoElementsDefinitions, pseudoName)));
                if (isPseudoElement) {
                    var signature = (_a = pseudoElementsDefinitions[pseudoName]) !== null && _a !== void 0 ? _a : (pseudoElementsAcceptUnknown && defaultPseudoSignature);
                    var pseudoElement = {
                        type: 'PseudoElement',
                        name: pseudoName
                    };
                    var argument = parsePseudoArgument(pseudoName, 'pseudo-element', signature);
                    if (argument) {
                        assert(argument.type !== 'Formula' && argument.type !== 'FormulaOfSelector', 'Pseudo-elements cannot have formula argument.');
                        pseudoElement.argument = argument;
                    }
                    rule.items.push(pseudoElement);
                }
                else {
                    assert(pseudoClassesEnabled, 'Pseudo-classes are not enabled.');
                    var signature = (_b = pseudoClassesDefinitions[pseudoName]) !== null && _b !== void 0 ? _b : (pseudoClassesAcceptUnknown && defaultPseudoSignature);
                    if (signature) {
                        // All good
                    }
                    else {
                        // Generate a helpful error message with location information
                        var locations = pseudoLocationIndex.pseudoClasses[pseudoName];
                        var errorMessage = "Unknown pseudo-class: \"".concat(pseudoName, "\"");
                        if (locations && locations.length > 0) {
                            errorMessage += ". It is defined in: ".concat(locations.join(', '));
                        }
                        fail(errorMessage + '.');
                    }
                    var argument = parsePseudoArgument(pseudoName, 'pseudo-class', signature);
                    var pseudoClass = {
                        type: 'PseudoClass',
                        name: pseudoName
                    };
                    if (argument) {
                        pseudoClass.argument = argument;
                    }
                    rule.items.push(pseudoClass);
                }
            }
            else {
                break;
            }
        }
        if (rule.items.length === 0) {
            if (isEof()) {
                return fail('Expected rule but end of input reached.');
            }
            else {
                return fail("Expected rule but \"".concat(chr, "\" found."));
            }
        }
        skipWhitespace();
        if (!isEof() && !is(',') && !is(')')) {
            var combinator = matchMulticharIndex(combinatorsIndex);
            skipWhitespace();
            rule.nestedRule = parseRule();
            rule.nestedRule.combinator = combinator;
        }
        return rule;
    }
    return function (input) {
        // noinspection SuspiciousTypeOfGuard
        if (typeof input !== 'string') {
            throw new Error("".concat(errorPrefix, "Expected string input."));
        }
        str = input;
        l = str.length;
        pos = 0;
        chr = str.charAt(0);
        return parseSelector();
    };
}
