"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.StylablePublicApi = exports.hooks = exports.diagnostics = exports.MixinType = void 0;
const feature_1 = require("./feature");
const STSymbol = __importStar(require("./st-symbol"));
const STCustomSelector = __importStar(require("./st-custom-selector"));
const STVar = __importStar(require("./st-var"));
const rule_1 = require("../helpers/rule");
const selector_1 = require("../helpers/selector");
const mixin_1 = require("../helpers/mixin");
const functions_1 = require("../functions");
const parser_1 = require("../parser");
const postcss = __importStar(require("postcss"));
const postcss_value_parser_1 = __importDefault(require("postcss-value-parser"));
const stylable_assets_1 = require("../stylable-assets");
const stylable_utils_1 = require("../stylable-utils");
const path_1 = require("path");
const diagnostics_1 = require("../diagnostics");
const css_selector_parser_1 = require("@tokey/css-selector-parser");
exports.MixinType = {
    ALL: `-st-mixin`,
    PARTIAL: `-st-partial-mixin`,
};
exports.diagnostics = {
    VALUE_CANNOT_BE_STRING: mixin_1.mixinHelperDiagnostics.VALUE_CANNOT_BE_STRING,
    INVALID_NAMED_PARAMS: mixin_1.mixinHelperDiagnostics.INVALID_NAMED_PARAMS,
    INVALID_MERGE_OF: stylable_utils_1.utilDiagnostics.INVALID_MERGE_OF,
    INVALID_RECURSIVE_MIXIN: stylable_utils_1.utilDiagnostics.INVALID_RECURSIVE_MIXIN,
    PARTIAL_MIXIN_MISSING_ARGUMENTS: (0, diagnostics_1.createDiagnosticReporter)('10001', 'error', (type) => `"${exports.MixinType.PARTIAL}" can only be used with override arguments provided, missing overrides on "${type}"`),
    UNKNOWN_MIXIN: (0, diagnostics_1.createDiagnosticReporter)('10002', 'error', (name) => `unknown mixin: "${name}"`),
    OVERRIDE_MIXIN: (0, diagnostics_1.createDiagnosticReporter)('10003', 'warning', (mixinType) => `override ${mixinType} on same rule`),
    FAILED_TO_APPLY_MIXIN: (0, diagnostics_1.createDiagnosticReporter)('10004', 'error', (error) => `could not apply mixin: ${error}`),
    JS_MIXIN_NOT_A_FUNC: (0, diagnostics_1.createDiagnosticReporter)('10005', 'error', () => `js mixin must be a function`),
    UNSUPPORTED_MIXIN_SYMBOL: (0, diagnostics_1.createDiagnosticReporter)('10007', 'error', (name, symbolType) => `cannot mix unsupported symbol "${name}" of type "${STSymbol.readableTypeMap[symbolType]}"`),
    CIRCULAR_MIXIN: (0, diagnostics_1.createDiagnosticReporter)('10006', 'error', (circularPaths) => `circular mixin found: ${circularPaths.join(' --> ')}`),
    UNKNOWN_ARG: (0, diagnostics_1.createDiagnosticReporter)('10009', 'warning', (argName) => `unknown mixin argument "${argName}"`),
};
// HOOKS
exports.hooks = (0, feature_1.createFeature)({
    transformSelectorNode({ selectorContext, node }) {
        const isMarker = (0, rule_1.isStMixinMarker)(node);
        if (isMarker) {
            selectorContext.setNextSelectorScope(selectorContext.inferredSelectorMixin, node, node.value);
        }
        return isMarker;
    },
    transformLastPass({ context, ast, transformer, path }) {
        ast.walkRules((rule) => appendMixins(context, transformer, rule, path));
    },
});
// API
class StylablePublicApi {
    constructor(stylable) {
        this.stylable = stylable;
    }
    resolveExpr(meta, expr, { diagnostics = new diagnostics_1.Diagnostics(), resolveOptionalArgs = false, } = {}) {
        const resolvedSymbols = this.stylable.resolver.resolveSymbols(meta, diagnostics);
        const { mainNamespace } = resolvedSymbols;
        const analyzedMixins = collectDeclMixins({ meta, diagnostics }, resolvedSymbols, postcss.decl({ prop: '-st-mixin', value: expr }), (mixinSymbolName) => (mainNamespace[mixinSymbolName] === 'js' ? 'args' : 'named'));
        const result = [];
        for (const { data } of analyzedMixins) {
            const name = data.type;
            const symbolKind = mainNamespace[name];
            if (symbolKind === 'class' || symbolKind === 'element') {
                const mixRef = {
                    name,
                    kind: 'css-fragment',
                    args: [],
                    optionalArgs: new Map(),
                };
                for (const [argName, argValue] of Object.entries(data.options)) {
                    mixRef.args.push({ [argName]: argValue });
                }
                if (resolveOptionalArgs) {
                    const varMap = new Map();
                    const resolveChain = resolvedSymbols[symbolKind][name];
                    getCSSMixinRoots(meta, resolveChain, ({ mixinRoot }) => {
                        const names = new Set();
                        collectOptionalArgs({ meta, resolver: this.stylable.resolver }, mixinRoot, names);
                        names.forEach((name) => varMap.set(name, { name }));
                    });
                    mixRef.optionalArgs = varMap;
                }
                result.push(mixRef);
            }
            else if (symbolKind === 'js' &&
                typeof resolvedSymbols.js[name].symbol === 'function') {
                const mixRef = {
                    name,
                    kind: 'js-func',
                    args: [],
                    func: resolvedSymbols.js[name].symbol,
                };
                for (const arg of Object.values(data.options)) {
                    mixRef.args.push(arg.value);
                }
                result.push(mixRef);
            }
            else {
                result.push({
                    name,
                    kind: 'invalid',
                    args: data.valueNode?.type === 'function'
                        ? postcss_value_parser_1.default.stringify(data.valueNode.nodes)
                        : '',
                });
            }
        }
        return result;
    }
    scopeNestedSelector(scopeSelector, nestSelector) {
        return (0, selector_1.scopeNestedSelector)((0, css_selector_parser_1.parseCssSelector)(scopeSelector), (0, css_selector_parser_1.parseCssSelector)(nestSelector))
            .selector;
    }
}
exports.StylablePublicApi = StylablePublicApi;
function appendMixins(context, transformer, rule, path = []) {
    const [decls, mixins] = collectRuleMixins(context, rule);
    if (!mixins || mixins.length === 0) {
        return;
    }
    for (const mixin of mixins) {
        if (mixin.valid) {
            appendMixin(context, { transformer, mixDef: mixin, rule, path });
        }
    }
    for (const mixinDecl of decls) {
        mixinDecl.remove();
    }
}
function collectRuleMixins(context, rule) {
    let mixins = [];
    const resolvedSymbols = context.getResolvedSymbols(context.meta);
    const { mainNamespace } = resolvedSymbols;
    const decls = [];
    for (const node of rule.nodes) {
        if (node.type === 'decl' &&
            (node.prop === `-st-mixin` || node.prop === `-st-partial-mixin`)) {
            decls.push(node);
            mixins = collectDeclMixins(context, resolvedSymbols, node, (mixinSymbolName) => {
                return mainNamespace[mixinSymbolName] === 'js' ? 'args' : 'named';
            }, mixins);
        }
    }
    return [decls, mixins];
}
function collectDeclMixins(context, resolvedSymbols, decl, paramSignature, previousMixins) {
    const { meta } = context;
    let mixins = [];
    const parser = decl.prop === exports.MixinType.ALL
        ? mixin_1.parseStMixin
        : decl.prop === exports.MixinType.PARTIAL
            ? mixin_1.parseStPartialMixin
            : null;
    if (!parser) {
        return previousMixins || mixins;
    }
    parser(decl, paramSignature, context.diagnostics, /*emitStrategyDiagnostics*/ true).forEach((mixin) => {
        const mixinRefSymbol = STSymbol.get(meta, mixin.type);
        const symbolName = mixin.type;
        const resolvedType = resolvedSymbols.mainNamespace[symbolName];
        if (resolvedType &&
            ((resolvedType === 'js' &&
                typeof resolvedSymbols.js[symbolName].symbol === 'function') ||
                resolvedType === 'class' ||
                resolvedType === 'element')) {
            mixins.push({
                valid: true,
                data: mixin,
                symbol: mixinRefSymbol,
            });
            if (mixin.partial && Object.keys(mixin.options).length === 0) {
                context.diagnostics.report(exports.diagnostics.PARTIAL_MIXIN_MISSING_ARGUMENTS(mixin.type), {
                    node: decl,
                    word: mixin.type,
                });
            }
        }
        else {
            mixins.push({
                valid: false,
                data: mixin,
                symbol: mixinRefSymbol,
            });
            if (resolvedType === 'js') {
                context.diagnostics.report(exports.diagnostics.JS_MIXIN_NOT_A_FUNC(), {
                    node: decl,
                    word: mixin.type,
                });
            }
            else if (resolvedType) {
                context.diagnostics.report(exports.diagnostics.UNSUPPORTED_MIXIN_SYMBOL(mixin.type, resolvedType), {
                    node: decl,
                    word: mixin.type,
                });
            }
            else {
                context.diagnostics.report(exports.diagnostics.UNKNOWN_MIXIN(mixin.type), {
                    node: decl,
                    word: mixin.type,
                });
            }
        }
    });
    if (previousMixins) {
        const partials = previousMixins.filter((r) => r.data.partial);
        const nonPartials = previousMixins.filter((r) => !r.data.partial);
        const isInPartial = decl.prop === exports.MixinType.PARTIAL;
        if ((partials.length && decl.prop === exports.MixinType.PARTIAL) ||
            (nonPartials.length && decl.prop === exports.MixinType.ALL)) {
            context.diagnostics.report(exports.diagnostics.OVERRIDE_MIXIN(decl.prop), { node: decl });
        }
        if (partials.length && nonPartials.length) {
            mixins = isInPartial ? nonPartials.concat(mixins) : partials.concat(mixins);
        }
        else if (partials.length) {
            mixins = isInPartial ? mixins : partials.concat(mixins);
        }
        else if (nonPartials.length) {
            mixins = isInPartial ? nonPartials.concat(mixins) : mixins;
        }
    }
    return mixins;
}
function appendMixin(context, config) {
    if (checkRecursive(context, config)) {
        return;
    }
    const resolvedSymbols = context.getResolvedSymbols(context.meta);
    const symbolName = config.mixDef.data.type;
    const resolvedType = resolvedSymbols.mainNamespace[symbolName];
    if (resolvedType === `class` || resolvedType === `element`) {
        const resolveChain = resolvedSymbols[resolvedType][symbolName];
        handleCSSMixin(context, config, resolveChain);
        return;
    }
    else if (resolvedType === `js`) {
        const jsValue = resolvedSymbols.js[symbolName].symbol;
        if (typeof jsValue === 'function') {
            try {
                handleJSMixin(context, config, jsValue);
            }
            catch (e) {
                context.diagnostics.report(exports.diagnostics.FAILED_TO_APPLY_MIXIN(String(e)), {
                    node: config.rule,
                    word: config.mixDef.data.type,
                });
                return;
            }
        }
        return;
    }
}
function checkRecursive({ meta, diagnostics: report }, { mixDef, path, rule }) {
    const symbolName = mixDef.symbol.name === meta.root
        ? mixDef.symbol._kind === 'class'
            ? meta.root
            : 'default'
        : mixDef.data.type;
    const isRecursive = path.includes(symbolName + ' from ' + meta.source);
    if (isRecursive) {
        // Todo: add test verifying word
        report.report(exports.diagnostics.CIRCULAR_MIXIN(path), {
            node: rule,
            word: symbolName,
        });
        return true;
    }
    return false;
}
function handleJSMixin(context, config, mixinFunction) {
    const stVarOverride = context.evaluator.stVarOverride || {};
    const meta = context.meta;
    const mixDef = config.mixDef;
    const res = mixinFunction(mixDef.data.options.map((v) => v.value));
    const mixinRoot = (0, parser_1.cssObjectToAst)(res);
    mixinRoot.walkDecls((decl) => {
        if (!(0, stylable_utils_1.isValidDeclaration)(decl)) {
            decl.value = String(decl);
        }
    });
    config.transformer.transformAst(mixinRoot, meta, undefined, stVarOverride, [], true);
    const mixinPath = mixDef.symbol.import.request;
    (0, stylable_assets_1.fixRelativeUrls)(mixinRoot, context.resolver.resolvePath((0, path_1.dirname)(meta.source), mixinPath), meta.source);
    (0, stylable_utils_1.mergeRules)(mixinRoot, config.rule, mixDef.data.originDecl, context.diagnostics, true);
}
function handleCSSMixin(context, config, resolveChain) {
    const mixDef = config.mixDef;
    const isPartial = mixDef.data.partial;
    const namedArgs = mixDef.data.options;
    const overrideKeys = Object.keys(namedArgs);
    if (isPartial && overrideKeys.length === 0) {
        return;
    }
    const optionalArgs = new Set();
    const roots = getCSSMixinRoots(context.meta, resolveChain, ({ mixinRoot, resolved, isRootMixin }) => {
        const stVarOverride = context.evaluator.stVarOverride || {};
        const mixDef = config.mixDef;
        const namedArgs = mixDef.data.options;
        if (mixDef.data.partial) {
            filterPartialMixinDecl(context.meta, mixinRoot, Object.keys(namedArgs));
        }
        // resolve override args
        const resolvedArgs = (0, functions_1.resolveArgumentsValue)(namedArgs, config.transformer, context.meta, context.diagnostics, mixDef.data.originDecl, stVarOverride, config.path);
        collectOptionalArgs({ meta: resolved.meta, resolver: context.resolver }, mixinRoot, optionalArgs);
        // transform mixin
        const mixinMeta = resolved.meta;
        const symbolName = isRootMixin && resolved.meta !== context.meta ? 'default' : mixDef.data.type;
        config.transformer.transformAst(mixinRoot, mixinMeta, undefined, resolvedArgs, config.path.concat(symbolName + ' from ' + context.meta.source), true, config.transformer.createInferredSelector(mixinMeta, {
            name: resolved.symbol.name,
            type: resolved.symbol._kind,
        }));
        (0, stylable_assets_1.fixRelativeUrls)(mixinRoot, resolved.meta.source, context.meta.source);
    });
    for (const overrideArg of overrideKeys) {
        if (!optionalArgs.has(overrideArg)) {
            context.diagnostics.report(exports.diagnostics.UNKNOWN_ARG(overrideArg), {
                node: mixDef.data.originDecl,
                word: overrideArg,
            });
        }
    }
    if (roots.length === 1) {
        (0, stylable_utils_1.mergeRules)(roots[0], config.rule, mixDef.data.originDecl, config.transformer.diagnostics, false);
    }
    else if (roots.length > 1) {
        const mixinRoot = postcss.root();
        roots.forEach((root) => mixinRoot.prepend(...root.nodes));
        (0, stylable_utils_1.mergeRules)(mixinRoot, config.rule, mixDef.data.originDecl, config.transformer.diagnostics, false);
    }
}
function collectOptionalArgs(context, mixinRoot, optionalArgs = new Set()) {
    mixinRoot.walk((node) => {
        const value = node.type === 'decl' ? node.value : node.type === 'atrule' ? node.params : '';
        const varNames = STVar.parseVarsFromExpr(value);
        for (const name of varNames) {
            for (const refName of STVar.resolveReferencedVarNames(context, name)) {
                optionalArgs.add(refName);
            }
        }
    });
}
function getCSSMixinRoots(contextMeta, resolveChain, processMixinRoot) {
    const roots = [];
    for (const resolved of resolveChain) {
        const isRootMixin = resolved.symbol.name === resolved.meta.root;
        const mixinRoot = (0, rule_1.createSubsetAst)(resolved.meta.sourceAst, (resolved.symbol._kind === 'class' ? '.' : '') + resolved.symbol.name, undefined, isRootMixin, (name) => STCustomSelector.getCustomSelector(contextMeta, name));
        processMixinRoot({ mixinRoot, resolved, isRootMixin });
        roots.push(mixinRoot);
        if (resolved.symbol[`-st-extends`]) {
            break;
        }
    }
    return roots;
}
/** we assume that mixinRoot is freshly created nodes from the ast */
function filterPartialMixinDecl(meta, mixinRoot, overrideKeys) {
    let regexp;
    const overrideSet = new Set(overrideKeys);
    let size;
    do {
        size = overrideSet.size;
        regexp = new RegExp(`value\\((\\s*${Array.from(overrideSet).join('\\s*)|(\\s*')}\\s*)\\)`);
        for (const { text, name } of Object.values(meta.getAllStVars())) {
            if (!overrideSet.has(name) && text.match(regexp)) {
                overrideSet.add(name);
            }
        }
    } while (overrideSet.size !== size);
    mixinRoot.walkDecls((decl) => {
        if (!decl.value.match(regexp)) {
            const parent = decl.parent;
            decl.remove();
            if (parent?.nodes?.length === 0) {
                parent.remove();
            }
        }
    });
}
//# sourceMappingURL=st-mixin.js.map