"use strict";
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    var desc = Object.getOwnPropertyDescriptor(m, k);
    if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
      desc = { enumerable: true, get: function() { return m[k]; } };
    }
    Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
    if (k2 === undefined) k2 = k;
    o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
    Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
    o["default"] = v;
});
var __importStar = (this && this.__importStar) || function (mod) {
    if (mod && mod.__esModule) return mod;
    var result = {};
    if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
    __setModuleDefault(result, mod);
    return result;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.parseStExtends = exports.extendTypedRule = exports.disableDirectivesForClass = exports.checkForScopedNodeAfter = exports.validateClassScoping = exports.createWarningRule = exports.addDevRules = exports.namespaceClass = exports.addClass = exports.createSymbol = exports.getAll = exports.get = exports.StylablePublicApi = exports.hooks = exports.diagnostics = void 0;
const feature_1 = require("./feature");
const diagnostics_1 = require("./diagnostics");
const STSymbol = __importStar(require("./st-symbol"));
const STCustomState = __importStar(require("./st-custom-state"));
const resolve_1 = require("../helpers/resolve");
const namespace_1 = require("../helpers/namespace");
const escape_1 = require("../helpers/escape");
const value_1 = require("../helpers/value");
const selector_1 = require("../helpers/selector");
const stylable_utils_1 = require("../stylable-utils");
const custom_state_1 = require("../helpers/custom-state");
const css_selector_parser_1 = require("@tokey/css-selector-parser");
const postcss = __importStar(require("postcss"));
const path_1 = require("path");
const diagnostics_2 = require("../diagnostics");
const postcss_value_parser_1 = __importDefault(require("postcss-value-parser"));
const plugable_record_1 = require("../helpers/plugable-record");
const stPartDirectives = {
    '-st-root': true,
    '-st-states': true,
    '-st-extends': true,
    '-st-global': true,
};
exports.diagnostics = {
    INVALID_FUNCTIONAL_SELECTOR: diagnostics_1.generalDiagnostics.INVALID_FUNCTIONAL_SELECTOR,
    UNSCOPED_CLASS: (0, diagnostics_2.createDiagnosticReporter)('00002', 'warning', (name) => `unscoped class "${name}" will affect all elements of the same type in the document`),
    STATE_DEFINITION_IN_ELEMENT: (0, diagnostics_2.createDiagnosticReporter)('11002', 'error', () => 'cannot define pseudo states inside a type selector'),
    STATE_DEFINITION_IN_COMPLEX: (0, diagnostics_2.createDiagnosticReporter)('11003', 'error', () => 'cannot define pseudo states inside complex selectors'),
    OVERRIDE_TYPED_RULE: (0, diagnostics_2.createDiagnosticReporter)('11006', 'warning', (key, name) => `override "${key}" on typed rule "${name}"`),
    CANNOT_RESOLVE_EXTEND: (0, diagnostics_2.createDiagnosticReporter)('11004', 'error', (name) => `cannot resolve '-st-extends' type for '${name}'`),
    CANNOT_EXTEND_IN_COMPLEX: (0, diagnostics_2.createDiagnosticReporter)('11005', 'error', () => `cannot define "-st-extends" inside a complex selector`),
    EMPTY_ST_GLOBAL: (0, diagnostics_2.createDiagnosticReporter)('00003', 'error', () => `-st-global must contain a valid selector`),
    UNSUPPORTED_MULTI_SELECTORS_ST_GLOBAL: (0, diagnostics_2.createDiagnosticReporter)('00004', 'error', () => `unsupported multi selector in -st-global`),
    UNSUPPORTED_COMPLEX_SELECTOR: (0, diagnostics_2.createDiagnosticReporter)('00010', 'error', () => `unsupported complex selector`),
    IMPORT_ISNT_EXTENDABLE: (0, diagnostics_2.createDiagnosticReporter)('00005', 'error', () => 'import is not extendable'),
    CANNOT_EXTEND_UNKNOWN_SYMBOL: (0, diagnostics_2.createDiagnosticReporter)('00006', 'error', (name) => `cannot extend unknown symbol "${name}"`),
    CANNOT_EXTEND_JS: (0, diagnostics_2.createDiagnosticReporter)('00007', 'error', () => 'JS import is not extendable'),
    UNKNOWN_IMPORT_ALIAS: (0, diagnostics_2.createDiagnosticReporter)('00008', 'error', (name) => `cannot use alias for unknown import "${name}"`),
    DISABLED_DIRECTIVE: (0, diagnostics_2.createDiagnosticReporter)('00009', 'error', (className, directive) => {
        const alternative = directive === '-st-extends'
            ? ` use "@st .${className} :is(.base)" instead`
            : directive === '-st-global'
                ? `use "@st .${className} => :global(<selector>)" instead`
                : directive === '-st-states'
                    ? `use "@st .${className} { @st .state; }" instead`
                    : '';
        return `cannot use ${directive} on .${className} since class is defined with "@st" - ${alternative}`;
    }),
};
const dataKey = plugable_record_1.plugableRecord.key('st-structure');
// HOOKS
exports.hooks = (0, feature_1.createFeature)({
    metaInit({ meta }) {
        plugable_record_1.plugableRecord.set(meta.data, dataKey, {
            classesDefinedWithAtSt: new Set(),
        });
    },
    analyzeSelectorNode({ context, node, rule }) {
        if (node.nodes) {
            // error on functional class
            context.diagnostics.report(exports.diagnostics.INVALID_FUNCTIONAL_SELECTOR(`.` + node.value, `class`), {
                node: rule,
                word: (0, selector_1.stringifySelector)(node),
            });
        }
        addClass(context, node.value, rule);
    },
    analyzeDeclaration({ context, decl }) {
        if (context.meta.type === 'stylable' && isDirectiveDeclaration(decl)) {
            handleDirectives(context, decl);
        }
    },
    transformResolve({ context }) {
        const resolvedSymbols = context.getResolvedSymbols(context.meta);
        const locals = {};
        for (const [localName, resolved] of Object.entries(resolvedSymbols.class)) {
            const exportedClasses = [];
            let first = true;
            // collect list of css classes for exports
            for (const { meta, symbol } of resolved) {
                if (!first && symbol[`-st-root`]) {
                    // extended stylesheet root: stop collection as root is expected to
                    // be placed by inner component, for example in <Button class={classes.primaryBtn} />
                    // `primaryBtn` shouldn't contain `button__root` as it is placed by the Button component
                    break;
                }
                first = false;
                if (symbol[`-st-global`]) {
                    // collect global override just in case of
                    // compound set of CSS classes
                    let isOnlyClasses = true;
                    const globalClasses = symbol[`-st-global`].reduce((globalClasses, node) => {
                        if (node.type === `class`) {
                            globalClasses.push(node.value);
                            context.meta.globals[node.value] = true;
                        }
                        else {
                            isOnlyClasses = false;
                        }
                        return globalClasses;
                    }, []);
                    if (isOnlyClasses) {
                        exportedClasses.push(...globalClasses);
                    }
                    continue;
                }
                if (symbol.alias && !symbol[`-st-extends`]) {
                    continue;
                }
                exportedClasses.push((0, namespace_1.namespace)(symbol.name, meta.namespace));
            }
            const classNames = (0, escape_1.unescapeCSS)(exportedClasses.join(' '));
            if (classNames) {
                const directResolve = resolved[0];
                const isLocal = directResolve.meta === context.meta && !directResolve.symbol.alias;
                locals[localName] = { classes: classNames, isLocal };
            }
        }
        return locals;
    },
    transformSelectorNode({ context, selectorContext, node }) {
        const { originMeta, resolver } = selectorContext;
        const resolvedSymbols = context.getResolvedSymbols(context.meta);
        const resolved = resolvedSymbols.class[node.value] || [
            // used to namespace classes from js mixins since js mixins
            // are scoped in the context of the mixed-in stylesheet
            // which might not have a definition for the mixed-in class
            { _kind: 'css', meta: originMeta, symbol: createSymbol({ name: node.value }) },
        ];
        selectorContext.setNextSelectorScope(resolved, node, node.value);
        const { symbol, meta } = (0, resolve_1.getOriginDefinition)(resolved);
        if (selectorContext.originMeta === meta && symbol[`-st-states`]) {
            // ToDo: refactor out to transformer validation phase
            (0, custom_state_1.validateRuleStateDefinition)(selectorContext.selectorStr, selectorContext.ruleOrAtRule, context.meta, resolver, context.diagnostics);
        }
        if (selectorContext.transform) {
            namespaceClass(meta, symbol, node);
        }
    },
    transformJSExports({ exports, resolved }) {
        for (const [localName, { classes, isLocal }] of Object.entries(resolved)) {
            if (isLocal) {
                exports.classes[localName] = classes;
            }
        }
    },
});
// API
class StylablePublicApi {
    constructor(stylable) {
        this.stylable = stylable;
    }
    transformIntoSelector(meta, name) {
        const localSymbol = STSymbol.get(meta, name);
        const resolved = localSymbol?._kind === 'import'
            ? this.stylable.resolver.deepResolve(localSymbol)
            : { _kind: 'css', meta, symbol: localSymbol };
        if (resolved?._kind !== 'css' || resolved.symbol?._kind !== 'class') {
            return undefined;
        }
        const node = {
            type: 'class',
            value: '',
            start: 0,
            end: 0,
            dotComments: [],
        };
        namespaceClass(resolved.meta, resolved.symbol, node, false);
        return (0, css_selector_parser_1.stringifySelectorAst)(node);
    }
}
exports.StylablePublicApi = StylablePublicApi;
function get(meta, name) {
    return STSymbol.get(meta, name, `class`);
}
exports.get = get;
function getAll(meta) {
    return STSymbol.getAllByType(meta, `class`);
}
exports.getAll = getAll;
function createSymbol(input) {
    const parts = input['-st-parts'] || {};
    return { ...input, _kind: 'class', '-st-parts': parts };
}
exports.createSymbol = createSymbol;
function addClass(context, name, rule) {
    let symbol = STSymbol.get(context.meta, name, `class`);
    if (!symbol) {
        let alias = STSymbol.get(context.meta, name);
        if (alias && alias._kind !== 'import') {
            alias = undefined;
        }
        symbol = STSymbol.addSymbol({
            context,
            symbol: createSymbol({ name, alias }),
            node: rule,
            safeRedeclare: !!alias,
        });
    }
    // mark native css as global
    if (context.meta.type === 'css' && !symbol['-st-global']) {
        symbol['-st-global'] = [
            {
                type: 'class',
                value: name,
                dotComments: [],
                start: 0,
                end: 0,
            },
        ];
    }
    return symbol;
}
exports.addClass = addClass;
function namespaceClass(meta, symbol, node, // ToDo: check this is the correct type, should this be inline selector?
wrapInGlobal = true) {
    if (`-st-global` in symbol && symbol[`-st-global`]) {
        // change node to `-st-global` value
        if (wrapInGlobal) {
            const globalMappedNodes = symbol[`-st-global`];
            (0, selector_1.convertToPseudoClass)(node, 'global', [
                {
                    type: 'selector',
                    nodes: globalMappedNodes,
                    after: '',
                    before: '',
                    end: 0,
                    start: 0,
                },
            ]);
        }
        else {
            const flatNode = (0, selector_1.convertToSelector)(node);
            const globalMappedNodes = symbol[`-st-global`];
            flatNode.nodes = globalMappedNodes;
        }
    }
    else {
        node = (0, selector_1.convertToClass)(node);
        node.value = (0, escape_1.namespaceEscape)(symbol.name, meta.namespace);
    }
}
exports.namespaceClass = namespaceClass;
function getNamespacedClass(meta, symbol) {
    if (`-st-global` in symbol && symbol[`-st-global`]) {
        const selector = symbol[`-st-global`];
        return (0, css_selector_parser_1.stringifySelectorAst)(selector);
    }
    else {
        return '.' + (0, escape_1.namespaceEscape)(symbol.name, meta.namespace);
    }
}
function addDevRules({ getResolvedSymbols, meta }) {
    const resolvedSymbols = getResolvedSymbols(meta);
    for (const resolved of Object.values(resolvedSymbols.class)) {
        const a = resolved[0];
        if (resolved.length > 1 && a.symbol['-st-extends']) {
            const b = resolved[resolved.length - 1];
            meta.targetAst.append(createWarningRule('.' + b.symbol.name, getNamespacedClass(b.meta, b.symbol), (0, path_1.basename)(b.meta.source), '.' + a.symbol.name, getNamespacedClass(a.meta, a.symbol), (0, path_1.basename)(a.meta.source)));
        }
    }
}
exports.addDevRules = addDevRules;
function createWarningRule(extendedNode, scopedExtendedNode, extendedFile, extendingNode, scopedExtendingNode, extendingFile) {
    const message = `"class extending component '${extendingNode} => ${scopedExtendingNode}' in stylesheet '${extendingFile}' was set on a node that does not extend '${extendedNode} => ${scopedExtendedNode}' from stylesheet '${extendedFile}'" !important`;
    return postcss.rule({
        raws: { between: ' ' },
        selector: `${scopedExtendingNode}:not(${scopedExtendedNode})::before`,
        nodes: [
            postcss.decl({
                prop: 'content',
                value: message,
            }),
            postcss.decl({
                prop: 'display',
                value: `block !important`,
            }),
            postcss.decl({
                prop: 'font-family',
                value: `monospace !important`,
            }),
            postcss.decl({
                prop: 'background-color',
                value: `red !important`,
            }),
            postcss.decl({
                prop: 'color',
                value: `white !important`,
            }),
        ],
    });
}
exports.createWarningRule = createWarningRule;
function validateClassScoping({ context, classSymbol, locallyScoped, reportUnscoped, node, nodes, index, rule, }) {
    if (context.meta.type !== 'stylable') {
        // ignore in native CSS
        return true;
    }
    if (!classSymbol.alias) {
        return true;
    }
    else if (locallyScoped === false) {
        if (checkForScopedNodeAfter(context, rule, nodes, index) === false) {
            if (reportUnscoped) {
                context.diagnostics.report(exports.diagnostics.UNSCOPED_CLASS(node.value), {
                    node: rule,
                    word: node.value,
                });
            }
            return false;
        }
        else {
            return true;
        }
    }
    return locallyScoped;
}
exports.validateClassScoping = validateClassScoping;
// ToDo: support more complex cases (e.g. `:is`)
function checkForScopedNodeAfter(context, rule, nodes, index) {
    for (let i = index + 1; i < nodes.length; i++) {
        const node = nodes[i];
        if (!node) {
            // ToDo: can this get here???
            break;
        }
        if (node.type === 'combinator') {
            break;
        }
        if (node.type === 'class') {
            const name = node.value;
            const classSymbol = addClass(context, name, rule);
            if (classSymbol && !classSymbol.alias) {
                return true;
            }
        }
    }
    return false;
}
exports.checkForScopedNodeAfter = checkForScopedNodeAfter;
function isDirectiveDeclaration(decl) {
    return decl.prop in stPartDirectives;
}
function disableDirectivesForClass(context, className) {
    // ToDo: move directive analyze to @st-structure
    // called when class is defined with @st
    const { classesDefinedWithAtSt } = plugable_record_1.plugableRecord.getUnsafe(context.meta.data, dataKey);
    classesDefinedWithAtSt.add(className);
}
exports.disableDirectivesForClass = disableDirectivesForClass;
function handleDirectives(context, decl) {
    const rule = decl.parent;
    if (rule?.type !== 'rule') {
        return;
    }
    const isSimplePerSelector = (0, selector_1.isSimpleSelector)(rule.selector);
    const type = isSimplePerSelector.reduce((accType, { type }) => {
        return !accType ? type : accType !== type ? `complex` : type;
    }, ``);
    const isSimple = type !== `complex`;
    const { classesDefinedWithAtSt } = plugable_record_1.plugableRecord.getUnsafe(context.meta.data, dataKey);
    if (type === 'class' && classesDefinedWithAtSt.has(rule.selector.replace('.', ''))) {
        context.diagnostics.report(exports.diagnostics.DISABLED_DIRECTIVE(rule.selector.replace('.', ''), decl.prop), {
            node: decl,
        });
        return;
    }
    else if (decl.prop === `-st-states`) {
        if (isSimple && type !== 'type') {
            extendTypedRule(context, decl, rule.selector, `-st-states`, STCustomState.parsePseudoStates(decl.value, decl, context.diagnostics));
        }
        else {
            if (type === 'type') {
                context.diagnostics.report(exports.diagnostics.STATE_DEFINITION_IN_ELEMENT(), {
                    node: decl,
                });
            }
            else {
                context.diagnostics.report(exports.diagnostics.STATE_DEFINITION_IN_COMPLEX(), {
                    node: decl,
                });
            }
        }
    }
    else if (decl.prop === `-st-extends`) {
        if (isSimple) {
            const parsed = parseStExtends(decl.value);
            const symbolName = parsed.types[0] && parsed.types[0].symbolName;
            const extendsRefSymbol = STSymbol.get(context.meta, symbolName);
            if ((extendsRefSymbol &&
                (extendsRefSymbol._kind === 'import' ||
                    extendsRefSymbol._kind === 'class' ||
                    extendsRefSymbol._kind === 'element')) ||
                decl.value === context.meta.root) {
                extendTypedRule(context, decl, rule.selector, `-st-extends`, (0, stylable_utils_1.getAlias)(extendsRefSymbol) || extendsRefSymbol);
            }
            else {
                context.diagnostics.report(exports.diagnostics.CANNOT_RESOLVE_EXTEND(decl.value), {
                    node: decl,
                    word: decl.value,
                });
            }
        }
        else {
            context.diagnostics.report(exports.diagnostics.CANNOT_EXTEND_IN_COMPLEX(), {
                node: decl,
            });
        }
    }
    else if (decl.prop === `-st-global`) {
        if (isSimple && type !== 'type') {
            // set class global mapping
            const name = rule.selector.replace('.', '');
            const classSymbol = get(context.meta, name);
            if (classSymbol) {
                const globalSelectorAst = parseStGlobal(context, decl);
                if (globalSelectorAst) {
                    classSymbol[`-st-global`] = globalSelectorAst;
                }
            }
        }
        else {
            // TODO: diagnostics - scoped on none class
        }
    }
}
function extendTypedRule(context, node, selector, key, value) {
    const name = selector.replace('.', '');
    const typedRule = STSymbol.get(context.meta, name);
    if (typedRule && typedRule[key]) {
        context.diagnostics.report(exports.diagnostics.OVERRIDE_TYPED_RULE(key, name), {
            node,
            word: name,
        });
    }
    if (typedRule) {
        typedRule[key] = value;
    }
}
exports.extendTypedRule = extendTypedRule;
function parseStExtends(value) {
    const ast = (0, postcss_value_parser_1.default)(value);
    const types = [];
    ast.walk((node) => {
        if (node.type === 'function') {
            const args = (0, value_1.getNamedArgs)(node);
            types.push({
                symbolName: node.value,
                args,
            });
            return false;
        }
        else if (node.type === 'word') {
            types.push({
                symbolName: node.value,
                args: null,
            });
        }
        return undefined;
    }, false);
    return {
        ast,
        types,
    };
}
exports.parseStExtends = parseStExtends;
function parseStGlobal(context, decl) {
    const selector = (0, selector_1.parseSelectorWithCache)(decl.value.replace(/^['"]/, '').replace(/['"]$/, ''), {
        clone: true,
    });
    if (!selector[0]) {
        context.diagnostics.report(exports.diagnostics.EMPTY_ST_GLOBAL(), {
            node: decl,
        });
        return;
    }
    else if (selector.length > 1) {
        context.diagnostics.report(exports.diagnostics.UNSUPPORTED_MULTI_SELECTORS_ST_GLOBAL(), {
            node: decl,
        });
        return;
    }
    else {
        for (const node of selector[0].nodes) {
            if (node.type === 'combinator') {
                context.diagnostics.report(exports.diagnostics.UNSUPPORTED_COMPLEX_SELECTOR(), {
                    node: decl,
                });
                return;
            }
        }
    }
    return selector[0].nodes;
}
//# sourceMappingURL=css-class.js.map