import isVendorPrefixed from 'is-vendor-prefixed';
import * as postcss from 'postcss';
import type { FileProcessor } from './cached-process-file';
import { createDiagnosticReporter, Diagnostics } from './diagnostics';
import { StylableEvaluator } from './functions';
import { nativePseudoElements } from './native-reserved-lists';
import {
    cloneSelector,
    createCombinatorSelector,
    parseSelectorWithCache,
    stringifySelector,
} from './helpers/selector';
import { isEqual } from './helpers/eql';
import {
    SelectorNode,
    Selector,
    SelectorList,
    groupCompoundSelectors,
    CompoundSelector,
    splitCompoundSelectors,
    ImmutableSelectorNode,
} from '@tokey/css-selector-parser';
import { isChildOfAtRule } from './helpers/rule';
import { getOriginDefinition } from './helpers/resolve';
import {
    ClassSymbol,
    CSSContains,
    CSSMedia,
    ElementSymbol,
    FeatureTransformContext,
    STNamespace,
    STStructure,
    STImport,
    STGlobal,
    STScope,
    STCustomSelector,
    STVar,
    STMixin,
    CSSClass,
    CSSType,
    CSSPseudoClass,
    CSSKeyframes,
    CSSLayer,
    CSSCustomProperty,
} from './features';
import type { StylableMeta } from './stylable-meta';
import {
    CSSResolve,
    StylableResolverCache,
    StylableResolver,
    createSymbolResolverWithCache,
} from './stylable-resolver';
import { validateCustomPropertyName } from './helpers/css-custom-property';
import type { ModuleResolver } from './types';
import { getRuleScopeSelector } from './deprecated/postcss-ast-extension';
import type { MappedStates } from './helpers/custom-state';

export interface ResolvedElement {
    name: string;
    type: string;
    resolved: Array<CSSResolve<ClassSymbol | ElementSymbol>>;
}

export type RuntimeStVar = string | { [key: string]: RuntimeStVar } | RuntimeStVar[];

export interface StylableExports {
    classes: Record<string, string>;
    vars: Record<string, string>;
    stVars: Record<string, RuntimeStVar>;
    keyframes: Record<string, string>;
    layers: Record<string, string>;
    containers: Record<string, string>;
}

export interface StylableResults {
    meta: StylableMeta;
    exports: StylableExports;
}

export type replaceValueHook = (
    value: string,
    name: string | { name: string; args: string[] },
    isLocal: boolean,
    passedThrough: string[]
) => string;

export type postProcessor<T = {}> = (
    stylableResults: StylableResults,
    transformer: StylableTransformer
) => StylableResults & T;

export interface TransformHooks {
    postProcessor?: postProcessor;
    replaceValueHook?: replaceValueHook;
}

type EnvMode = 'production' | 'development';

export interface TransformerOptions {
    fileProcessor: FileProcessor<StylableMeta>;
    moduleResolver: ModuleResolver;
    requireModule: (modulePath: string) => any;
    diagnostics: Diagnostics;
    keepValues?: boolean;
    replaceValueHook?: replaceValueHook;
    postProcessor?: postProcessor;
    mode?: EnvMode;
    resolverCache?: StylableResolverCache;
    stVarOverride?: Record<string, string>;
    experimentalSelectorInference?: boolean;
}

export const transformerDiagnostics = {
    UNKNOWN_PSEUDO_ELEMENT: createDiagnosticReporter(
        '12001',
        'error',
        (name: string) => `unknown pseudo element "${name}"`
    ),
};

type PostcssContainer = postcss.Container<postcss.ChildNode> | postcss.Document;

export class StylableTransformer {
    public fileProcessor: FileProcessor<StylableMeta>;
    public diagnostics: Diagnostics;
    public resolver: StylableResolver;
    public keepValues: boolean;
    public replaceValueHook: replaceValueHook | undefined;
    public postProcessor: postProcessor | undefined;
    public mode: EnvMode;
    private defaultStVarOverride: Record<string, string>;
    private evaluator: StylableEvaluator;
    public getResolvedSymbols: ReturnType<typeof createSymbolResolverWithCache>;
    private directiveNodes: postcss.Declaration[] = [];
    public experimentalSelectorInference: boolean;
    public containerInferredSelectorMap = new Map<PostcssContainer, InferredSelector>();
    constructor(options: TransformerOptions) {
        this.diagnostics = options.diagnostics;
        this.keepValues = options.keepValues || false;
        this.fileProcessor = options.fileProcessor;
        this.replaceValueHook = options.replaceValueHook;
        this.postProcessor = options.postProcessor;
        this.experimentalSelectorInference = options.experimentalSelectorInference === true;
        this.resolver = new StylableResolver(
            options.fileProcessor,
            options.requireModule,
            options.moduleResolver,
            options.resolverCache || new Map()
        );
        this.mode = options.mode || 'production';
        this.defaultStVarOverride = options.stVarOverride || {};
        this.getResolvedSymbols = createSymbolResolverWithCache(this.resolver, this.diagnostics);
        this.evaluator = new StylableEvaluator({
            valueHook: this.replaceValueHook,
            getResolvedSymbols: this.getResolvedSymbols,
        });
    }
    public transform(meta: StylableMeta): StylableResults {
        meta.exports = {
            classes: {},
            vars: {},
            stVars: {},
            keyframes: {},
            layers: {},
            containers: {},
        };
        meta.transformedScopes = null;
        meta.targetAst = meta.sourceAst.clone();
        const context = {
            meta,
            diagnostics: this.diagnostics,
            resolver: this.resolver,
            evaluator: this.evaluator,
            getResolvedSymbols: this.getResolvedSymbols,
        };
        STImport.hooks.transformInit({ context });
        STGlobal.hooks.transformInit({ context });
        if (!this.experimentalSelectorInference) {
            meta.transformedScopes = validateScopes(this, meta);
        }
        this.transformAst(meta.targetAst, meta, meta.exports);
        meta.transformDiagnostics = this.diagnostics;
        const result = { meta, exports: meta.exports };

        return this.postProcessor ? this.postProcessor(result, this) : result;
    }
    public transformAst(
        ast: postcss.Root,
        meta: StylableMeta,
        metaExports?: StylableExports,
        stVarOverride: Record<string, string> = this.defaultStVarOverride,
        path: string[] = [],
        mixinTransform = false,
        inferredSelectorMixin?: InferredSelector
    ) {
        if (meta.type !== 'stylable') {
            return;
        }
        const { evaluator } = this;

        const prevStVarOverride = evaluator.stVarOverride;
        evaluator.stVarOverride = stVarOverride;

        const transformContext = {
            meta,
            diagnostics: this.diagnostics,
            resolver: this.resolver,
            evaluator,
            getResolvedSymbols: this.getResolvedSymbols,
            passedThrough: path.slice(),
            inferredSelectorMixin,
        };
        const transformResolveOptions = {
            context: transformContext,
        };
        prepareAST(transformContext, ast, this.experimentalSelectorInference);

        const cssClassResolve = CSSClass.hooks.transformResolve(transformResolveOptions);
        const stVarResolve = STVar.hooks.transformResolve(transformResolveOptions);
        const keyframesResolve = CSSKeyframes.hooks.transformResolve(transformResolveOptions);
        const layerResolve = CSSLayer.hooks.transformResolve(transformResolveOptions);
        const containsResolve = CSSContains.hooks.transformResolve(transformResolveOptions);
        const cssVarsMapping = CSSCustomProperty.hooks.transformResolve(transformResolveOptions);

        const handleAtRule = (atRule: postcss.AtRule) => {
            const { name } = atRule;
            if (name === 'media') {
                CSSMedia.hooks.transformAtRuleNode({
                    context: transformContext,
                    atRule,
                    resolved: {},
                    transformer: this,
                });
            } else if (name === 'property') {
                CSSCustomProperty.hooks.transformAtRuleNode({
                    context: transformContext,
                    atRule,
                    resolved: cssVarsMapping,
                    transformer: this,
                });
            } else if (name === 'keyframes') {
                CSSKeyframes.hooks.transformAtRuleNode({
                    context: transformContext,
                    atRule,
                    resolved: keyframesResolve,
                    transformer: this,
                });
            } else if (name === 'layer') {
                CSSLayer.hooks.transformAtRuleNode({
                    context: transformContext,
                    atRule,
                    resolved: layerResolve,
                    transformer: this,
                });
            } else if (name === 'import') {
                CSSLayer.hooks.transformAtRuleNode({
                    context: transformContext,
                    atRule,
                    resolved: layerResolve,
                    transformer: this,
                });
            } else if (name === 'container') {
                CSSContains.hooks.transformAtRuleNode({
                    context: transformContext,
                    atRule,
                    resolved: containsResolve,
                    transformer: this,
                });
            } else if (name === 'st-scope') {
                STScope.hooks.transformAtRuleNode({
                    context: transformContext,
                    atRule,
                    resolved: containsResolve,
                    transformer: this,
                });
            } else if (name === 'custom-selector') {
                STCustomSelector.hooks.transformAtRuleNode({
                    context: transformContext,
                    atRule,
                    resolved: containsResolve,
                    transformer: this,
                });
            }
        };
        const handleDeclaration = (decl: postcss.Declaration) => {
            if (validateCustomPropertyName(decl.prop)) {
                CSSCustomProperty.hooks.transformDeclaration({
                    context: transformContext,
                    decl,
                    resolved: cssVarsMapping,
                });
            } else if (decl.prop === `animation` || decl.prop === `animation-name`) {
                CSSKeyframes.hooks.transformDeclaration({
                    context: transformContext,
                    decl,
                    resolved: keyframesResolve,
                });
            } else if (decl.prop === 'container' || decl.prop === 'container-name') {
                CSSContains.hooks.transformDeclaration({
                    context: transformContext,
                    decl,
                    resolved: containsResolve,
                });
            }

            if (decl.prop.startsWith('-st-')) {
                if (this.mode === 'production') {
                    this.directiveNodes.push(decl);
                }
                return;
            }

            decl.value = this.evaluator.evaluateValue(transformContext, {
                value: decl.value,
                meta,
                node: decl,
            }).outputValue;
        };

        ast.walk((node) => {
            if (node.type === 'rule') {
                if (isChildOfAtRule(node, 'keyframes')) {
                    return;
                }
                // get context inferred selector
                let currentParent: PostcssContainer | undefined = node.parent;
                while (currentParent && !this.containerInferredSelectorMap.has(currentParent)) {
                    currentParent = currentParent.parent;
                }
                // transform selector
                const { selector, inferredSelector } = this.scopeSelector(
                    meta,
                    node.selector,
                    node,
                    currentParent && this.containerInferredSelectorMap.get(currentParent),
                    inferredSelectorMixin
                );
                // save results
                this.containerInferredSelectorMap.set(node, inferredSelector);
                node.selector = selector;
            } else if (node.type === 'atrule') {
                handleAtRule(node);
            } else if (node.type === 'decl') {
                handleDeclaration(node);
            }
        });

        if (!mixinTransform && meta.targetAst && this.mode === 'development') {
            CSSClass.addDevRules(transformContext);
        }

        const lastPassParams = {
            context: transformContext,
            ast,
            transformer: this,
            path,
        };
        if (this.experimentalSelectorInference) {
            STScope.hooks.transformLastPass(lastPassParams);
        }
        STMixin.hooks.transformLastPass(lastPassParams);
        if (!mixinTransform) {
            STGlobal.hooks.transformLastPass(lastPassParams);
            for (const node of this.directiveNodes) {
                node.remove();
            }
        }

        if (metaExports) {
            CSSClass.hooks.transformJSExports({
                exports: metaExports,
                resolved: cssClassResolve,
            });
            STVar.hooks.transformJSExports({
                exports: metaExports,
                resolved: stVarResolve,
            });
            CSSKeyframes.hooks.transformJSExports({
                exports: metaExports,
                resolved: keyframesResolve,
            });
            CSSLayer.hooks.transformJSExports({
                exports: metaExports,
                resolved: layerResolve,
            });
            CSSContains.hooks.transformJSExports({
                exports: metaExports,
                resolved: containsResolve,
            });
            CSSCustomProperty.hooks.transformJSExports({
                exports: metaExports,
                resolved: cssVarsMapping,
            });
        }

        // restore evaluator state
        this.evaluator.stVarOverride = prevStVarOverride;
    }
    public resolveSelectorElements(meta: StylableMeta, selector: string): ResolvedElement[][] {
        return this.scopeSelector(meta, selector).elements;
    }
    public scopeSelector(
        originMeta: StylableMeta,
        selector: string,
        selectorNode?: postcss.Rule | postcss.AtRule,
        inferredNestSelector?: InferredSelector,
        inferredMixinSelector?: InferredSelector,
        unwrapGlobals = false
    ): {
        selector: string;
        elements: ResolvedElement[][];
        targetSelectorAst: SelectorList;
        inferredSelector: InferredSelector;
    } {
        const context = this.createSelectorContext(
            originMeta,
            parseSelectorWithCache(selector, { clone: true }),
            selectorNode || postcss.rule({ selector }),
            selector,
            inferredNestSelector,
            inferredMixinSelector
        );
        const targetSelectorAst = this.scopeSelectorAst(context);
        if (unwrapGlobals) {
            STGlobal.unwrapPseudoGlobals(targetSelectorAst);
        }
        return {
            targetSelectorAst,
            selector: stringifySelector(targetSelectorAst),
            elements: context.elements,
            inferredSelector: context.inferredMultipleSelectors,
        };
    }
    public createSelectorContext(
        meta: StylableMeta,
        selectorAst: SelectorList,
        selectorNode: postcss.Rule | postcss.AtRule,
        selectorStr?: string,
        selectorNest?: InferredSelector,
        selectorMixin?: InferredSelector
    ) {
        return new ScopeContext(
            meta,
            this.resolver,
            selectorAst,
            selectorNode,
            this.scopeSelectorAst.bind(this),
            this,
            selectorNest,
            selectorMixin,
            undefined,
            selectorStr
        );
    }
    public createInferredSelector(
        meta: StylableMeta,
        { name, type }: { name: string; type: 'class' | 'element' }
    ) {
        const resolvedSymbols = this.getResolvedSymbols(meta);
        const resolved = resolvedSymbols[type][name];
        return new InferredSelector(this, resolved);
    }
    public scopeSelectorAst(context: ScopeContext): SelectorList {
        // group compound selectors: .a.b .c:hover, a .c:hover -> [[[.a.b], [.c:hover]], [[.a], [.c:hover]]]
        const selectorList = groupCompoundSelectors(context.selectorAst);
        // loop over selectors
        for (const selector of selectorList) {
            context.elements.push([]);
            context.selectorIndex++;
            context.selector = selector;
            // loop over nodes
            for (const node of [...selector.nodes]) {
                if (node.type !== `compound_selector`) {
                    if (node.type === 'combinator') {
                        if (this.experimentalSelectorInference || context.isNested) {
                            context.setNextSelectorScope(context.inferredSelectorContext, node);
                        }
                    }
                    continue;
                }
                context.compoundSelector = node;
                // loop over each node in a compound selector
                for (const compoundNode of node.nodes) {
                    if (compoundNode.type === 'universal' && this.experimentalSelectorInference) {
                        context.setNextSelectorScope(
                            [
                                {
                                    _kind: 'css',
                                    meta: context.originMeta,
                                    symbol: CSSType.createSymbol({ name: '*' }),
                                },
                            ],
                            node
                        );
                    }
                    context.node = compoundNode;
                    // transform node
                    this.handleCompoundNode(context as Required<ScopeContext>);
                }
            }
            // add inferred selector end to multiple selector
            context.inferredMultipleSelectors.add(context.inferredSelector);
            if (selectorList.length - 1 > context.selectorIndex) {
                // reset current anchor for all except last selector
                context.inferredSelector = new InferredSelector(
                    this,
                    context.inferredSelectorStart
                );
            }
        }
        // backwards compatibility for elements - empty selector still have an empty first target
        if (selectorList.length === 0) {
            context.elements.push([]);
        }
        const targetAst = splitCompoundSelectors(selectorList);
        context.splitSelectors.duplicateSelectors(targetAst);
        for (let i = 0; i < targetAst.length; i++) {
            context.selectorAst[i] = targetAst[i];
        }
        return targetAst;
    }
    private handleCompoundNode(context: Required<ScopeContext>) {
        const { inferredSelector, node, originMeta } = context;
        const transformerContext = {
            meta: originMeta,
            diagnostics: this.diagnostics,
            resolver: this.resolver,
            evaluator: this.evaluator,
            getResolvedSymbols: this.getResolvedSymbols,
        };
        if (node.type === 'class') {
            CSSClass.hooks.transformSelectorNode({
                context: transformerContext,
                selectorContext: context,
                node,
            });
        } else if (node.type === 'type') {
            CSSType.hooks.transformSelectorNode({
                context: transformerContext,
                selectorContext: context,
                node,
            });
        } else if (node.type === 'pseudo_element') {
            if (node.value === ``) {
                // partial psuedo elemennt: `.x::`
                // ToDo: currently the transformer corrects the css without warning,
                // should stylable warn?
                return;
            }
            const inferredElement = inferredSelector.getPseudoElements({
                isFirstInSelector: context.isFirstInSelector(node),
                name: node.value,
                experimentalSelectorInference: this.experimentalSelectorInference,
            })[node.value];
            if (inferredElement) {
                context.setNextSelectorScope(inferredElement.inferred, node, node.value);
                if (context.transform) {
                    context.transformIntoMultiSelector(node, inferredElement.selectors);
                }
            } else {
                // first definition of a part in the extends/alias chain
                context.setNextSelectorScope([], node, node.value);

                if (
                    !nativePseudoElements.includes(node.value) &&
                    !isVendorPrefixed(node.value) &&
                    !context.isDuplicateStScopeDiagnostic()
                ) {
                    this.diagnostics.report(
                        transformerDiagnostics.UNKNOWN_PSEUDO_ELEMENT(node.value),
                        {
                            node: context.ruleOrAtRule,
                            word: node.value,
                        }
                    );
                }
            }
        } else if (node.type === 'pseudo_class') {
            const isCustomSelector = STCustomSelector.hooks.transformSelectorNode({
                context: transformerContext,
                selectorContext: context,
                node,
            });
            if (!isCustomSelector) {
                CSSPseudoClass.hooks.transformSelectorNode({
                    context: transformerContext,
                    selectorContext: context,
                    node,
                });
            }
        } else if (node.type === `nesting`) {
            context.setNextSelectorScope(context.inferredSelectorNest, node, node.value);
        } else if (node.type === 'attribute') {
            STMixin.hooks.transformSelectorNode({
                context: transformerContext,
                selectorContext: context,
                node,
            });
        }
    }
}

function validateScopes(transformer: StylableTransformer, meta: StylableMeta) {
    const transformedScopes: Record<string, SelectorList> = {};
    for (const scope of meta.scopes) {
        const len = transformer.diagnostics.reports.length;
        const rule = postcss.rule({ selector: scope.params });

        const context = transformer.createSelectorContext(
            meta,
            parseSelectorWithCache(rule.selector, { clone: true }),
            rule,
            rule.selector
        );
        transformedScopes[rule.selector] = groupCompoundSelectors(
            transformer.scopeSelectorAst(context)
        );
        const ruleReports = transformer.diagnostics.reports.splice(len);

        for (const { code, message, severity, word } of ruleReports) {
            transformer.diagnostics.report(
                {
                    code,
                    message,
                    severity,
                },
                {
                    node: scope,
                    word: word || scope.params,
                }
            );
        }
    }

    return transformedScopes;
}

function removeInitialCompoundMarker(
    selector: Selector,
    meta: StylableMeta,
    structureMode: boolean
) {
    let hadCompoundStart = false;
    const compoundedSelector = groupCompoundSelectors(selector);
    const first = compoundedSelector.nodes.find(
        ({ type }) => type === `compound_selector`
    ) as CompoundSelector;
    if (first) {
        const matchNode = structureMode
            ? (node: SelectorNode) => node.type === 'nesting'
            : (node: SelectorNode) => node.type === 'class' && node.value === meta.root;
        for (let i = 0; i < first.nodes.length; i++) {
            const node = first.nodes[i];
            if (node.type === 'comment') {
                continue;
            }
            if (matchNode(node)) {
                hadCompoundStart = true;
                first.nodes.splice(i, 1);
            }
            break;
        }
    }
    return { selector: splitCompoundSelectors(compoundedSelector), hadCompoundStart };
}

type SelectorSymbol = ClassSymbol | ElementSymbol | STStructure.PartSymbol;
type InferredResolve = CSSResolve<SelectorSymbol>;
type InferredPseudoElement = {
    inferred: InferredSelector;
    selectors: SelectorList;
};
type InferredPseudoClass = {
    meta: StylableMeta;
    state: MappedStates[string];
};
export class InferredSelector {
    protected resolveSet = new Set<InferredResolve[]>();
    constructor(
        private api: Pick<
            StylableTransformer,
            | 'getResolvedSymbols'
            | 'createSelectorContext'
            | 'scopeSelectorAst'
            | 'createInferredSelector'
        >,
        resolve?: InferredResolve[] | InferredSelector
    ) {
        if (resolve) {
            this.add(resolve);
        }
    }
    public isEmpty() {
        return this.resolveSet.size === 0;
    }
    public set(resolve: InferredResolve[] | InferredSelector) {
        if (resolve === this) {
            return;
        }
        this.resolveSet.clear();
        this.add(resolve);
    }
    public clone() {
        return new InferredSelector(this.api, this);
    }
    /**
     * Adds to the set of inferred resolved CSS
     * Assumes passes CSSResolved from the same meta/symbol are
     * the same from the same cached transform process to dedupe them.
     */
    public add(resolve: InferredResolve[] | InferredSelector) {
        if (resolve instanceof InferredSelector) {
            resolve.resolveSet.forEach((resolve) => this.add(resolve));
        } else {
            this.resolveSet.add(resolve);
        }
    }
    /**
     * Takes a CSS part resolve and use it extend the current set of inferred resolved.
     * Used to expand the resolved mapped selector with the part definition
     * e.g. part can add nested states/parts that override the inferred mapped selector.
     */
    private addPartOverride(partResolve: CSSResolve<STStructure.PartSymbol>) {
        const newSet = new Set<InferredResolve[]>();
        for (const resolve of this.resolveSet) {
            newSet.add([partResolve, ...resolve]);
        }
        if (!this.resolveSet.size) {
            newSet.add([partResolve]);
        }
        this.resolveSet = newSet;
    }
    public getPseudoClasses({ name: searchedName }: { name?: string } = {}) {
        const collectedStates: Record<string, InferredPseudoClass> = {};
        const resolvedCount: Record<string, number> = {};
        const expectedIntersectionCount = this.resolveSet.size; // ToDo: dec for any types
        const addInferredState = (
            name: string,
            meta: StylableMeta,
            state: MappedStates[string]
        ) => {
            const existing = collectedStates[name];
            if (!existing) {
                collectedStates[name] = { meta, state };
                resolvedCount[name] = 1;
            } else {
                const isStatesEql = isEqual(existing.state, state);
                if (
                    isStatesEql &&
                    // states from same meta
                    (existing.meta === meta ||
                        // global states
                        typeof state === 'string' ||
                        state?.type === 'template')
                ) {
                    resolvedCount[name]++;
                }
            }
        };
        // infer states from  multiple resolved selectors
        for (const resolvedContext of this.resolveSet.values()) {
            const resolvedFoundNames = new Set<string>();
            resolved: for (const { symbol, meta } of resolvedContext) {
                const states = symbol[`-st-states`];
                if (!states) {
                    continue;
                }
                if (searchedName) {
                    if (Object.hasOwnProperty.call(states, searchedName)) {
                        // track state
                        addInferredState(searchedName, meta, states[searchedName]);
                        break resolved;
                    }
                } else {
                    // get all states
                    for (const [name, state] of Object.entries(states)) {
                        if (!resolvedFoundNames.has(name)) {
                            // track state
                            resolvedFoundNames.add(name);
                            addInferredState(name, meta, state);
                        }
                    }
                }
            }
        }
        // strict: remove states that do not exist on ALL resolved selectors
        return expectedIntersectionCount > 1
            ? Object.entries(collectedStates).reduce((resultStates, [name, InferredState]) => {
                  if (resolvedCount[name] >= expectedIntersectionCount) {
                      resultStates[name] = InferredState;
                  }
                  return resultStates;
              }, {} as typeof collectedStates)
            : collectedStates;
    }
    public getPseudoElements({
        isFirstInSelector,
        experimentalSelectorInference,
        name,
    }: {
        isFirstInSelector: boolean;
        experimentalSelectorInference: boolean;
        name?: string;
    }) {
        const collectedElements: Record<string, InferredPseudoElement> = {};
        const resolvedCount: Record<string, number> = {};
        const checked: Record<string, Map<string, boolean>> = {};
        const expectedIntersectionCount = this.resolveSet.size; // ToDo: dec for any types
        const addInferredElement = (
            name: string,
            inferred: InferredSelector,
            selectors: SelectorList
        ) => {
            const item = (collectedElements[name] ||= {
                inferred: new InferredSelector(this.api),
                selectors: [],
            });
            // check inferred matching
            if (!item.inferred.matchedElement(inferred)) {
                // ToDo: bailout fast
                return;
            }
            // add match
            resolvedCount[name]++;
            item.inferred.add(inferred);
            item.selectors.push(...selectors);
        };
        // infer elements from  multiple resolved selectors
        for (const resolvedContext of this.resolveSet.values()) {
            /**
             * search for elements in each resolved selector.
             * start at 1 for legacy flat mode to prefer inherited elements over local
             */
            const startIndex =
                resolvedContext.length === 1 ||
                (resolvedContext[0] &&
                    (STStructure.isStructureMode(resolvedContext[0].meta) ||
                        resolvedContext[0].symbol._kind === 'part'))
                    ? 0
                    : 1;
            resolved: for (let i = startIndex; i < resolvedContext.length; i++) {
                const { symbol, meta } = resolvedContext[i];
                const structureMode = STStructure.isStructureMode(meta);
                if (
                    symbol._kind !== 'part' &&
                    (symbol.alias || (!structureMode && !symbol['-st-root']))
                ) {
                    // non-root & alias classes don't have parts: bailout
                    continue;
                }
                if (name) {
                    const cacheContext = symbol._kind === 'part' ? symbol.id : symbol.name;
                    const uniqueId = meta.source + '::' + cacheContext;
                    resolvedCount[name] ??= 0;
                    checked[name] ||= new Map();
                    if (checked[name].has(uniqueId)) {
                        if (checked[name].get(uniqueId)) {
                            resolvedCount[name]++;
                        }
                        continue;
                    }
                    // get part symbol
                    const partDef = STStructure.getPart(symbol, name);
                    // save to cache
                    checked[name].set(uniqueId, !!partDef);

                    if (!partDef) {
                        continue;
                    }
                    if (Array.isArray(partDef.mapTo)) {
                        // prefer custom selector
                        const selectorList = cloneSelector(partDef.mapTo);
                        const selectorStr = stringifySelector(partDef.mapTo);
                        selectorList.forEach((selector) => {
                            const r = removeInitialCompoundMarker(selector, meta, structureMode);
                            selector.nodes = r.selector.nodes;
                            selector.before = '';
                            if (!r.hadCompoundStart && !isFirstInSelector) {
                                selector.nodes.unshift(
                                    createCombinatorSelector({ combinator: 'space' })
                                );
                            }
                        });
                        const internalContext = this.api.createSelectorContext(
                            meta,
                            selectorList,
                            postcss.rule({ selector: selectorStr }),
                            selectorStr
                        );
                        internalContext.isStandaloneSelector = isFirstInSelector;
                        if (!structureMode && experimentalSelectorInference) {
                            internalContext.inferredSelectorStart.set(
                                this.api.createInferredSelector(meta, {
                                    name: 'root',
                                    type: 'class',
                                })
                            );
                            internalContext.inferredSelector.set(
                                internalContext.inferredSelectorStart
                            );
                        }
                        const customAstSelectors = this.api.scopeSelectorAst(internalContext);
                        const inferred =
                            customAstSelectors.length === 1 || experimentalSelectorInference
                                ? internalContext.inferredMultipleSelectors
                                : new InferredSelector(this.api, [
                                      {
                                          _kind: 'css',
                                          meta,
                                          symbol: CSSType.createSymbol({ name: '*' }),
                                      },
                                  ]);
                        // add part resolve to inferred resolve set
                        if (structureMode) {
                            inferred.addPartOverride({ _kind: 'css', meta, symbol: partDef });
                        }
                        addInferredElement(name, inferred, customAstSelectors);
                        break resolved;
                    } else {
                        // matching class part
                        const resolvedPart = this.api.getResolvedSymbols(meta).class[name];
                        const resolvedBaseSymbol = getOriginDefinition(resolvedPart);
                        const nodes: SelectorNode[] = [];
                        // insert descendant combinator before internal custom element
                        if (!resolvedBaseSymbol.symbol[`-st-root`] && !isFirstInSelector) {
                            nodes.push(createCombinatorSelector({ combinator: 'space' }));
                        }
                        // create part class
                        const classNode = {} as SelectorNode;
                        CSSClass.namespaceClass(
                            resolvedBaseSymbol.meta,
                            resolvedBaseSymbol.symbol,
                            classNode
                        );
                        nodes.push(classNode);

                        addInferredElement(name, new InferredSelector(this.api, resolvedPart), [
                            { type: 'selector', after: '', before: '', end: 0, start: 0, nodes },
                        ]);
                        break resolved;
                    }
                } else {
                    // ToDo: implement get all elements
                }
            }
        }
        // strict: remove elements that do not exist on ALL resolved selectors
        return expectedIntersectionCount > 1
            ? Object.entries(collectedElements).reduce(
                  (resultElements, [name, InferredElement]) => {
                      if (resolvedCount[name] >= expectedIntersectionCount) {
                          resultElements[name] = InferredElement;
                      }
                      return resultElements;
                  },
                  {} as typeof collectedElements
              )
            : collectedElements;
    }
    private matchedElement(inferred: InferredSelector): boolean {
        for (const target of this.resolveSet) {
            const targetBaseElementSymbol = getOriginDefinition(target);
            for (const tested of inferred.resolveSet) {
                const testedBaseElementSymbol = getOriginDefinition(tested);
                if (targetBaseElementSymbol !== testedBaseElementSymbol) {
                    return false;
                }
            }
        }
        return true;
    }
    // function to temporarily handle single resolved selector type while refactoring
    //      ToDo: remove temporarily single resolve
    getSingleResolve(): InferredResolve[] {
        if (this.resolveSet.size !== 1) {
            return [];
        }
        return this.resolveSet.values().next().value;
    }
}

class SelectorMultiplier {
    private dupIndicesPerSelector: [nodeIndex: number, selectors: SelectorList][][] = [];
    public addSplitPoint(selectorIndex: number, nodeIndex: number, selectors: SelectorList) {
        if (selectors.length) {
            this.dupIndicesPerSelector[selectorIndex] ||= [];
            this.dupIndicesPerSelector[selectorIndex].push([nodeIndex, selectors]);
        }
    }
    public duplicateSelectors(targetSelectors: SelectorList) {
        // iterate top level selector
        for (const [selectorIndex, insertionPoints] of Object.entries(this.dupIndicesPerSelector)) {
            const duplicationList = [targetSelectors[Number(selectorIndex)]];
            // iterate insertion points
            for (const [nodeIndex, selectors] of insertionPoints) {
                // collect the duplicate selectors to be multiplied by following insertion points
                const added: SelectorList = [];
                // iterate selectors for insertion point
                for (const replaceSelector of selectors) {
                    // duplicate selectors and replace selector at insertion point
                    for (const originSelector of duplicationList) {
                        const dupSelector = { ...originSelector, nodes: [...originSelector.nodes] };
                        dupSelector.nodes[nodeIndex] = replaceSelector;
                        added.push(dupSelector);
                    }
                }
                // add the duplicated selectors from insertion point to
                // the list of selector to be duplicated for following insertion
                // points and to the target selector list
                for (const addedSelector of added) {
                    duplicationList.push(addedSelector);
                    targetSelectors.push(addedSelector);
                }
            }
        }
    }
}

export class ScopeContext {
    public transform = true;
    // source multi-selector input
    public selectorStr = '';
    public selectorIndex = -1;
    public elements: any[] = [];
    public selectorAstResolveMap = new Map<ImmutableSelectorNode, InferredSelector>();
    public selector?: Selector;
    public compoundSelector?: CompoundSelector;
    public node?: CompoundSelector['nodes'][number];
    // true for nested selector
    public isNested: boolean;
    // store selector duplication points
    public splitSelectors = new SelectorMultiplier();
    public lastInferredSelectorNode: SelectorNode | undefined;
    // selector is not a continuation of another selector
    public isStandaloneSelector = true;
    // used as initial selector
    public inferredSelectorStart: InferredSelector;
    // used as initial selector or after combinators
    public inferredSelectorContext: InferredSelector;
    // used for nesting selector
    public inferredSelectorNest: InferredSelector;
    // current type while traversing a selector
    public inferredSelector: InferredSelector;
    // combined type of the multiple selectors
    public inferredMultipleSelectors: InferredSelector = new InferredSelector(this.transformer);
    constructor(
        public originMeta: StylableMeta,
        public resolver: StylableResolver,
        public selectorAst: SelectorList,
        public ruleOrAtRule: postcss.Rule | postcss.AtRule,
        public scopeSelectorAst: StylableTransformer['scopeSelectorAst'],
        private transformer: StylableTransformer,
        inferredSelectorNest?: InferredSelector,
        public inferredSelectorMixin?: InferredSelector,
        inferredSelectorContext?: InferredSelector,
        selectorStr?: string
    ) {
        this.isNested = !!(
            ruleOrAtRule.parent &&
            // top level
            ruleOrAtRule.parent.type !== 'root' &&
            // directly in @st-scope
            !STScope.isStScopeStatement(ruleOrAtRule.parent)
        );
        /* 
            resolve default selector context for initial selector and selector
            following a combinator.
            
            Currently set to stylesheet root for top level selectors and selectors
            directly nested under @st-scope. But will change in the future to a universal selector
            once experimentalSelectorInference will be the default behavior
        */
        const inferredContext =
            inferredSelectorContext ||
            (this.isNested || transformer.experimentalSelectorInference
                ? new InferredSelector(transformer, [
                      {
                          _kind: 'css',
                          meta: originMeta,
                          symbol: CSSType.createSymbol({ name: '*' }),
                      },
                  ])
                : transformer.createInferredSelector(originMeta, {
                      name: originMeta.root,
                      type: 'class',
                  }));
        // set selector data
        this.selectorStr = selectorStr || stringifySelector(selectorAst);
        this.inferredSelectorContext = new InferredSelector(this.transformer, inferredContext);
        this.inferredSelectorStart = new InferredSelector(this.transformer, inferredContext);
        this.inferredSelectorNest = inferredSelectorNest || this.inferredSelectorContext.clone();
        this.inferredSelector = new InferredSelector(
            this.transformer,
            this.inferredSelectorContext
        );
    }
    get experimentalSelectorInference() {
        return this.transformer.experimentalSelectorInference;
    }
    static legacyElementsTypesMapping: Record<string, string> = {
        pseudo_element: 'pseudo-element',
        class: 'class',
        type: 'element',
    };
    public setNextSelectorScope(
        resolved: InferredResolve[] | InferredSelector,
        node: SelectorNode,
        name?: string
    ) {
        if (name && this.selectorIndex !== undefined && this.selectorIndex !== -1) {
            this.elements[this.selectorIndex].push({
                type: ScopeContext.legacyElementsTypesMapping[node.type] || 'unknown',
                name,
                resolved: Array.isArray(resolved) ? resolved : resolved.getSingleResolve(),
            });
        }
        this.inferredSelector.set(resolved);
        this.selectorAstResolveMap.set(node, this.inferredSelector.clone());
        this.lastInferredSelectorNode = node;
    }
    public isFirstInSelector(node: SelectorNode) {
        const isFirstNode = this.selectorAst[this.selectorIndex].nodes[0] === node;
        if (isFirstNode && this.selectorIndex === 0 && !this.isStandaloneSelector) {
            // force false incase a this context is a splitted part from another selector
            return false;
        }
        return isFirstNode;
    }
    public createNestedContext(selectorAst: SelectorList, selectorContext?: InferredSelector) {
        const ctx = new ScopeContext(
            this.originMeta,
            this.resolver,
            selectorAst,
            this.ruleOrAtRule,
            this.scopeSelectorAst,
            this.transformer,
            this.inferredSelectorNest,
            this.inferredSelectorMixin,
            selectorContext || this.inferredSelectorContext
        );
        ctx.transform = this.transform;
        ctx.selectorAstResolveMap = this.selectorAstResolveMap;

        return ctx;
    }
    public transformIntoMultiSelector(node: SelectorNode, selectors: SelectorList) {
        // transform into the first selector
        Object.assign(node, selectors[0]);
        // keep track of additional selectors for
        // duplication at the end of the selector transform
        selectors.shift();
        const selectorNode = this.selectorAst[this.selectorIndex];
        const nodeIndex = selectorNode.nodes.indexOf(node);
        this.splitSelectors.addSplitPoint(this.selectorIndex, nodeIndex, selectors);
    }
    public isDuplicateStScopeDiagnostic() {
        if (this.experimentalSelectorInference || this.ruleOrAtRule.type !== 'rule') {
            // this check is not required when experimentalSelectorInference is on
            // as @st-scope is not flatten at the beginning of the transformation
            // and diagnostics on it's selector is only checked once.
            return false;
        }
        // ToDo: should be removed once st-scope transformation moves to the end of the transform process
        const transformedScope =
            this.originMeta.transformedScopes?.[getRuleScopeSelector(this.ruleOrAtRule) || ``];
        if (transformedScope && this.selector && this.compoundSelector) {
            const currentCompoundSelector = stringifySelector(this.compoundSelector);
            const i = this.selector.nodes.indexOf(this.compoundSelector);
            for (const stScopeSelectorCompounded of transformedScope) {
                // if we are in a chunk index that is in the rage of the @st-scope param
                if (i <= stScopeSelectorCompounded.nodes.length) {
                    for (const scopeNode of stScopeSelectorCompounded.nodes) {
                        const scopeNodeSelector = stringifySelector(scopeNode);
                        // if the two chunks match the error is already reported by the @st-scope validation
                        if (scopeNodeSelector === currentCompoundSelector) {
                            return true;
                        }
                    }
                }
            }
        }
        return false;
    }
}

/**
 * in the process of moving transformations that shouldn't be in the analyzer.
 * all changes were moved here to be called at the beginning of the transformer,
 * and should be inlined in the process in the future.
 */
function prepareAST(
    context: FeatureTransformContext,
    ast: postcss.Root,
    experimentalSelectorInference: boolean
) {
    // ToDo: inline transformations
    const toRemove: Array<postcss.Node | (() => void)> = [];
    ast.walk((node) => {
        const input = { context, node, toRemove };
        STNamespace.hooks.prepareAST(input);
        STImport.hooks.prepareAST(input);
        if (!experimentalSelectorInference) {
            STScope.hooks.prepareAST(input);
        }
        STVar.hooks.prepareAST(input);
        if (!experimentalSelectorInference) {
            STCustomSelector.hooks.prepareAST(input);
        }
        CSSCustomProperty.hooks.prepareAST(input);
    });
    for (const removeOrNode of toRemove) {
        typeof removeOrNode === 'function' ? removeOrNode() : removeOrNode.remove();
    }
}
