import type * as postcss from 'postcss';
import postcssValueParser, {
    type Node as ValueNode,
    type FunctionNode,
} from 'postcss-value-parser';
import cssesc from 'cssesc';
import type { PseudoClass, SelectorList, SelectorNode } from '@tokey/css-selector-parser';
import { createDiagnosticReporter, Diagnostics } from '../diagnostics';
import {
    parseSelectorWithCache,
    stringifySelector,
    convertToClass,
    convertToInvalid,
    convertToSelector,
} from './selector';
import { groupValues, listOptions } from './value';
import { stripQuotation } from './string';
import { evalDeclarationValue } from '../functions';
import type { StylableMeta } from '../stylable-meta';
import type { StylableResolver } from '../stylable-resolver';
import type { ParsedValue } from '../types';
import { CSSClass } from '../features';
import { reservedFunctionalPseudoClasses } from '../native-reserved-lists';
import { BaseAstNode, stringifyCSSValue } from '@tokey/css-value-parser';
import { findCustomIdent, findNextCallNode } from './css-value-seeker';

export interface MappedStates {
    [s: string]: StateParsedValue | string | TemplateStateParsedValue | null;
}
export interface TemplateStateParsedValue {
    type: 'template';
    template: string;
    params: [StateParsedValue];
}
export interface StateParsedValue {
    type: string;
    defaultValue?: string;
    arguments: StateArguments;
}
export interface StateTypeValidator {
    name: string;
    args: string[];
}

type StateArguments = Array<StateTypeValidator | string>;

export const stateMiddleDelimiter = '-';
export const booleanStateDelimiter = '--';
export const stateWithParamDelimiter = booleanStateDelimiter + stateMiddleDelimiter;

export const stateDiagnostics = {
    MISSING_TYPE_OR_TEMPLATE: createDiagnosticReporter(
        '08000',
        'error',
        (name: string) => `pseudo-state "${name}" missing type or template`
    ),
    UNKNOWN_STATE_TYPE: createDiagnosticReporter(
        '08002',
        'error',
        (name: string, type: string) =>
            `pseudo-state "${name}" defined with unknown type: "${type}"`
    ),
    TOO_MANY_STATE_TYPES: createDiagnosticReporter(
        '08003',
        'error',
        (name: string, types: string[]) =>
            `pseudo-state "${name}(${types.join(', ')})" definition must be of a single type`
    ),
    NO_STATE_TYPE_GIVEN: createDiagnosticReporter(
        '08005',
        'warning',
        (name: string) =>
            `pseudo-state "${name}" expected a definition of a single type, but received none`
    ),
    TOO_MANY_ARGS_IN_VALIDATOR: createDiagnosticReporter(
        '08006',
        'error',
        (name: string, validator: string, args: string[]) =>
            `pseudo-state "${name}" expected "${validator}" validator to receive a single argument, but it received "${args.join(
                ', '
            )}"`
    ),
    STATE_STARTS_WITH_HYPHEN: createDiagnosticReporter(
        '08007',
        'error',
        (name: string) =>
            `state "${name}" declaration cannot begin with a "${stateMiddleDelimiter}" character`
    ),
    RESERVED_NATIVE_STATE: createDiagnosticReporter(
        '08008',
        'warning',
        (name: string) => `state "${name}" is reserved for native pseudo-class`
    ),
    DEFAULT_PARAM_FAILS_VALIDATION: createDiagnosticReporter(
        '08010',
        'error',
        (stateName: string, defaultValue: string, errors: string[]) =>
            `pseudo-state "${stateName}" default value "${defaultValue}" failed validation:\n${errors.join(
                '\n'
            )}`
    ),
    NO_STATE_ARGUMENT_GIVEN: createDiagnosticReporter(
        '08004',
        'error',
        (name: string, type: string) =>
            `pseudo-state "${name}" expected argument of type "${type}" but got none`
    ),
    FAILED_STATE_VALIDATION: createDiagnosticReporter(
        '08009',
        'error',
        (name: string, actualParam: string, errors: string[]) =>
            [
                `pseudo-state "${name}" with parameter "${actualParam}" failed validation:`,
                ...errors,
            ].join('\n')
    ),
    TEMPLATE_MISSING_PLACEHOLDER: createDiagnosticReporter(
        '08011',
        'warning',
        (state: string, template: string) =>
            `pseudo-state "${state}" template "${template}" is missing a placeholder, use "$0" to set the parameter insertion place`
    ),
    TEMPLATE_MULTI_PARAMETERS: createDiagnosticReporter(
        '08012',
        'error',
        (state: string) => `pseudo-state "${state}" template only supports a single parameter`
    ),
    TEMPLATE_MISSING_PARAMETER: createDiagnosticReporter(
        '08013',
        'error',
        (state: string) => `pseudo-state "${state}" template expected a parameter definition`
    ),
    UNSUPPORTED_MULTI_SELECTOR: createDiagnosticReporter(
        '08014',
        'error',
        (state: string, finalSelector: string) =>
            `pseudo-state "${state}" resulted in an unsupported multi selector "${finalSelector}"`
    ),
    UNSUPPORTED_COMPLEX_SELECTOR: createDiagnosticReporter(
        '08015',
        'error',
        (state: string, finalSelector: string) =>
            `pseudo-state "${state}" resulted in an unsupported complex selector "${finalSelector}"`
    ),
    INVALID_SELECTOR: createDiagnosticReporter(
        '08016',
        'error',
        (state: string, finalSelector: string) =>
            `pseudo-state "${state}" resulted in an invalid selector "${finalSelector}"`
    ),
    UNSUPPORTED_INITIAL_SELECTOR: createDiagnosticReporter(
        '08017',
        'error',
        (state: string, finalSelector: string) =>
            `pseudo-state "${state}" result cannot start with a type or universal selector "${finalSelector}"`
    ),
    NO_PARAM_REQUIRED: createDiagnosticReporter(
        '08018',
        'error',
        (name: string, param: string) =>
            `pseudo-state "${name}" accepts no parameter, but received "${param}"`
    ),
};

// parse

export function parsePseudoStates(
    value: string,
    decl: postcss.Declaration,
    diagnostics: Diagnostics
) {
    const mappedStates: MappedStates = {};
    const ast = postcssValueParser(value);
    const statesSplitByComma = groupValues(ast.nodes);

    statesSplitByComma.forEach((workingState: ParsedValue[]) => {
        const [stateDefinition, ...stateDefault] = workingState;
        const stateName = stateDefinition.value;
        if (!validateStateName(stateName, diagnostics, decl)) {
            return;
        }

        if (stateDefinition.type === 'function') {
            resolveStateType(
                stateDefinition as FunctionNode,
                mappedStates,
                stateDefault,
                diagnostics,
                decl
            );
        } else if (stateDefinition.type === 'word') {
            resolveBooleanState(mappedStates, stateDefinition);
        } else {
            // TODO: Invalid state, edge case needs warning
        }
    });

    return mappedStates;
}
function validateStateName(name: string, diagnostics: Diagnostics, node: postcss.Node) {
    if (name.startsWith('-')) {
        diagnostics.report(stateDiagnostics.STATE_STARTS_WITH_HYPHEN(name), {
            node: node,
            word: name,
        });
    } else if (reservedFunctionalPseudoClasses.includes(name)) {
        diagnostics.report(stateDiagnostics.RESERVED_NATIVE_STATE(name), {
            node: node,
            word: name,
        });
        return false;
    }
    return true;
}
export function parseStateValue(
    value: BaseAstNode[],
    node: postcss.Node,
    diagnostics: Diagnostics
): [amountTaken: number, stateDef: MappedStates[string] | undefined] {
    let stateName = '';
    let stateDef: MappedStates[string] = null; /*boolean*/
    let amountTaken = 0;
    const customIdentResult = findCustomIdent(value, 0);
    const [amountToName, nameNode] = customIdentResult[0]
        ? customIdentResult
        : findNextCallNode(value, 0);
    if (nameNode && validateStateName(nameNode.value, diagnostics, node)) {
        amountTaken += amountToName;
        stateName = nameNode.value;
        // state with parameter
        if (nameNode.type === 'call') {
            // take all of the definition since default value takes the rest
            amountTaken = value.length;
            // ToDo: translate resolveStateType to tokey and remove the double parsing
            const postcssStateValue = postcssValueParser(
                stringifyCSSValue(value.slice(amountToName - 1))
            );
            // get state definition
            const [stateDefinition, ...stateDefault] = postcssStateValue.nodes;
            const stateMap: MappedStates = {};
            resolveStateType(
                stateDefinition as FunctionNode,
                stateMap,
                stateDefault,
                diagnostics,
                node as postcss.Declaration // ToDo: change to accept any postcss node
            );
            if (stateMap[stateName]) {
                stateDef = stateMap[stateName];
            }
        }
    }
    if (stateName) {
        return [amountTaken, stateDef];
    }
    return [0, undefined];
}
function resolveBooleanState(mappedStates: MappedStates, stateDefinition: ParsedValue) {
    const currentState = mappedStates[stateDefinition.value];
    if (!currentState) {
        mappedStates[stateDefinition.value] = null; // add boolean state
    } else {
        // TODO: warn with such name already exists
    }
}
function resolveStateType(
    stateDefinition: FunctionNode,
    mappedStates: MappedStates,
    stateDefault: ParsedValue[],
    diagnostics: Diagnostics,
    decl: postcss.Declaration
) {
    const stateName = stateDefinition.value;
    if (stateDefinition.nodes.length === 0) {
        resolveBooleanState(mappedStates, stateDefinition);

        diagnostics.report(stateDiagnostics.NO_STATE_TYPE_GIVEN(stateName), {
            node: decl,
            word: decl.value,
        });

        return;
    }
    const { paramType, argsFirstNode, argsFullValue } = collectStateArgsDef(stateDefinition.nodes);

    if (!paramType) {
        diagnostics.report(stateDiagnostics.MISSING_TYPE_OR_TEMPLATE(stateName), {
            node: decl,
        });
        return;
    }

    if (paramType?.type === 'string') {
        defineTemplateState(
            stateName,
            paramType,
            argsFirstNode,
            argsFullValue,
            mappedStates,
            diagnostics,
            decl
        );
    } else {
        if (argsFullValue.length > 1) {
            diagnostics.report(
                stateDiagnostics.TOO_MANY_STATE_TYPES(
                    stateName,
                    argsFirstNode.map((argNode) =>
                        argNode ? postcssValueParser.stringify(argNode) : ''
                    )
                ),
                {
                    node: decl,
                    word: decl.value,
                }
            );
        }
        defineParamState(
            stateName,
            paramType,
            stateDefault,
            mappedStates,
            diagnostics,
            stateDefinition,
            decl
        );
    }
}
function defineTemplateState(
    stateName: string,
    templateDef: postcssValueParser.StringNode,
    argsFirstNode: (postcssValueParser.Node | undefined)[],
    argsFullValue: postcssValueParser.Node[][],
    mappedStates: MappedStates,
    diagnostics: Diagnostics,
    decl: postcss.Declaration
) {
    const template = stripQuotation(postcssValueParser.stringify(templateDef));
    if (argsFullValue.length === 1) {
        // simple template with no params
        const selectorStr = template.trim().replace(/\\["']/g, '"');
        const selectorAst = parseSelectorWithCache(selectorStr, { clone: true });
        if (
            !validateTemplateSelector({
                stateName,
                selectorStr,
                selectorAst,
                cssNode: decl,
                diagnostics,
            })
        ) {
            return;
        } else {
            mappedStates[stateName] = selectorStr;
        }
    } else if (argsFullValue.length === 2) {
        // single parameter template
        if (!template.includes('$0')) {
            diagnostics.report(stateDiagnostics.TEMPLATE_MISSING_PLACEHOLDER(stateName, template), {
                node: decl,
                word: template,
            });
        }

        const paramFullDef = argsFullValue[1];
        const paramTypeDef = argsFirstNode[1];
        if (!paramTypeDef) {
            diagnostics.report(stateDiagnostics.TEMPLATE_MISSING_PARAMETER(stateName), {
                node: decl,
            });
            return;
        }
        const param = createStateParamDef(
            stateName + ' parameter',
            paramTypeDef,
            paramFullDef.splice(paramFullDef.indexOf(paramTypeDef) + 1),
            diagnostics,
            decl
        );
        if (!param) {
            // UNKNOWN_STATE_TYPE reported in createStateParamDef
            return;
        }

        const templateStateType: TemplateStateParsedValue = {
            type: 'template',
            template,
            params: [param],
        };

        mappedStates[stateName] = templateStateType;
    } else {
        // unsupported multiple params
        diagnostics.report(stateDiagnostics.TEMPLATE_MULTI_PARAMETERS(stateName), {
            node: decl,
        });
    }
}
function defineParamState(
    stateName: string,
    paramType: postcssValueParser.Node,
    stateDefault: ParsedValue[],
    mappedStates: MappedStates,
    diagnostics: Diagnostics,
    stateDefinition: FunctionNode,
    decl: postcss.Declaration
) {
    if (paramType.value === 'boolean') {
        // explicit boolean // ToDo: remove support
        resolveBooleanState(mappedStates, stateDefinition);
    } else {
        const stateParamDef = createStateParamDef(
            stateName,
            paramType,
            stateDefault,
            diagnostics,
            decl
        );
        if (stateParamDef) {
            mappedStates[stateName] = stateParamDef;
        }
    }
}
function createStateParamDef(
    stateName: string,
    typeDef: postcssValueParser.Node,
    stateDefault: ParsedValue[],
    diagnostics: Diagnostics,
    decl: postcss.Declaration
): StateParsedValue | undefined {
    const type = typeDef.value;
    if (type in systemValidators && (typeDef.type === 'function' || typeDef.type === 'word')) {
        const stateType: StateParsedValue = {
            type,
            arguments: [],
            defaultValue: postcssValueParser
                .stringify(stateDefault as postcssValueParser.Node[])
                .trim(),
        };
        if (typeDef.type === 'function' && typeDef.nodes.length > 0) {
            resolveArguments(typeDef, stateType, stateName, diagnostics, decl);
        }
        return stateType;
    } else {
        const srcValue = postcssValueParser.stringify(typeDef);
        diagnostics.report(stateDiagnostics.UNKNOWN_STATE_TYPE(stateName, srcValue), {
            node: decl,
            word: srcValue,
        });
        return;
    }
}
function collectStateArgsDef(nodes: ValueNode[]) {
    const argsFullValue: ValueNode[][] = [];
    const argsFirstNode: Array<ValueNode | undefined> = [];
    let collectedArg: ValueNode[] = [];
    let firstActualValue: ValueNode | undefined = undefined;

    for (const node of nodes) {
        if (node.type === 'div') {
            argsFullValue.push(collectedArg);
            argsFirstNode.push(firstActualValue);
            collectedArg = [];
            firstActualValue = undefined;
        } else {
            collectedArg.push(node);
            if (!firstActualValue && node.type !== 'space' && node.type !== 'comment') {
                firstActualValue = node;
            }
        }
    }

    if (collectedArg.length) {
        argsFullValue.push(collectedArg);
        argsFirstNode.push(firstActualValue);
    }

    const paramType = argsFirstNode[0];

    return { paramType, argsFullValue, argsFirstNode };
}
function resolveArguments(
    paramType: ParsedValue,
    stateType: StateParsedValue,
    name: string,
    diagnostics: Diagnostics,
    decl: postcss.Declaration
) {
    const separatedByComma = groupValues(paramType.nodes);

    separatedByComma.forEach((group) => {
        const validator = group[0];
        if (validator.type === 'function') {
            const args = listOptions(validator);
            if (args.length > 1) {
                diagnostics.report(
                    stateDiagnostics.TOO_MANY_ARGS_IN_VALIDATOR(name, validator.value, args),
                    {
                        node: decl,
                        word: decl.value,
                    }
                );
            } else {
                stateType.arguments.push({
                    name: validator.value,
                    args,
                });
            }
        } else if (validator.type === 'string' || validator.type === 'word') {
            stateType.arguments.push(validator.value);
        }
    });
}

// validation

export interface StateResult {
    res: string;
    errors: string[] | null;
}

export const validationErrors = {
    string: {
        STRING_TYPE_VALIDATION_FAILED: (actualParam: string) =>
            `"${actualParam}" should be of type string`,
        REGEX_VALIDATION_FAILED: (regex: string, actualParam: string) =>
            `expected "${actualParam}" to match regex "${regex}"`,
        CONTAINS_VALIDATION_FAILED: (shouldContain: string, actualParam: string) =>
            `expected "${actualParam}" to contain string "${shouldContain}"`,
        MIN_LENGTH_VALIDATION_FAILED: (length: string, actualParam: string) =>
            `expected "${actualParam}" to be of length longer than or equal to ${length}`,
        MAX_LENGTH_VALIDATION_FAILED: (length: string, actualParam: string) =>
            `expected "${actualParam}" to be of length shorter than or equal to ${length}`,
        UKNOWN_VALIDATOR: (name: string) => `encountered unknown string validator "${name}"`,
    },
    number: {
        NUMBER_TYPE_VALIDATION_FAILED: (actualParam: string) =>
            `expected "${actualParam}" to be of type number`,
        MIN_VALIDATION_FAILED: (actualParam: string, min: string) =>
            `expected "${actualParam}" to be larger than or equal to ${min}`,
        MAX_VALIDATION_FAILED: (actualParam: string, max: string) =>
            `expected "${actualParam}" to be lesser then or equal to ${max}`,
        MULTIPLE_OF_VALIDATION_FAILED: (actualParam: string, multipleOf: string) =>
            `expected "${actualParam}" to be a multiple of ${multipleOf}`,
        UKNOWN_VALIDATOR: (name: string) => `encountered unknown number validator "${name}"`,
    },
    enum: {
        ENUM_TYPE_VALIDATION_FAILED: (actualParam: string, options: string[]) =>
            `expected "${actualParam}" to be one of the options: "${options.join(', ')}"`,
        NO_OPTIONS_DEFINED: () => `expected enum to be defined with one option or more`,
    },
};

export type SubValidator = (value: string, ...rest: string[]) => StateResult;

export interface StateParamType {
    subValidators?: Record<string, SubValidator>;
    validate(
        value: any,
        args: StateArguments,
        resolveParam: any,
        validateDefinition: boolean,
        validateValue: boolean
    ): StateResult;
}

export const systemValidators: Record<string, StateParamType> = {
    string: {
        validate(
            value: any,
            validators: StateArguments,
            resolveParam: (s: string) => string,
            validateDefinition,
            validateValue
        ) {
            const res = value;
            const errors: string[] = [];

            if (validateValue && typeof value !== 'string') {
                errors.push(validationErrors.string.STRING_TYPE_VALIDATION_FAILED(value));
            }

            if (validators.length > 0) {
                validators.forEach((validatorMeta) => {
                    if (typeof validatorMeta === 'object') {
                        if (this.subValidators && this.subValidators[validatorMeta.name]) {
                            const subValidator = this.subValidators[validatorMeta.name];

                            const validationRes = subValidator(
                                value,
                                resolveParam(validatorMeta.args[0])
                            );

                            if (validateValue && validationRes.errors) {
                                errors.push(...validationRes.errors);
                            }
                        } else if (validateDefinition) {
                            errors.push(
                                validationErrors.string.UKNOWN_VALIDATOR(validatorMeta.name)
                            );
                        }
                    }
                });
            }

            return { res, errors: errors.length ? errors : null };
        },
        subValidators: {
            regex: (value: string, regex: string) => {
                const r = new RegExp(regex);
                const valid = r.test(value);

                return {
                    res: value,
                    errors: valid
                        ? null
                        : [validationErrors.string.REGEX_VALIDATION_FAILED(regex, value)],
                };
            },
            contains: (value: string, checkedValue: string) => {
                const valid = !!~value.indexOf(checkedValue);

                return {
                    res: value,
                    errors: valid
                        ? null
                        : [validationErrors.string.CONTAINS_VALIDATION_FAILED(checkedValue, value)],
                };
            },
            minLength: (value: string, length: string) => {
                const valid = value.length >= Number(length);

                return {
                    res: value,
                    errors: valid
                        ? null
                        : [validationErrors.string.MIN_LENGTH_VALIDATION_FAILED(length, value)],
                };
            },
            maxLength: (value: string, length: string) => {
                const valid = value.length <= Number(length);

                return {
                    res: value,
                    errors: valid
                        ? null
                        : [validationErrors.string.MAX_LENGTH_VALIDATION_FAILED(length, value)],
                };
            },
        },
    },
    number: {
        validate(
            value: any,
            validators: StateArguments,
            resolveParam: (s: string) => string,
            validateDefinition,
            validateValue
        ) {
            const res = value;
            const errors: string[] = [];

            if (isNaN(value)) {
                if (validateValue) {
                    errors.push(validationErrors.number.NUMBER_TYPE_VALIDATION_FAILED(value));
                }
            } else if (validators.length > 0) {
                validators.forEach((validatorMeta) => {
                    if (typeof validatorMeta === 'object') {
                        if (this.subValidators && this.subValidators[validatorMeta.name]) {
                            const subValidator = this.subValidators[validatorMeta.name];

                            const validationRes = subValidator(
                                value,
                                resolveParam(validatorMeta.args[0])
                            );

                            if (validateValue && validationRes.errors) {
                                errors.push(...validationRes.errors);
                            }
                        } else if (validateDefinition) {
                            errors.push(
                                validationErrors.number.UKNOWN_VALIDATOR(validatorMeta.name)
                            );
                        }
                    }
                });
            }

            return { res, errors: errors.length ? errors : null };
        },
        subValidators: {
            min: (value: string, minValue: string) => {
                const valid = Number(value) >= Number(minValue);

                return {
                    res: value,
                    errors: valid
                        ? null
                        : [validationErrors.number.MIN_VALIDATION_FAILED(value, minValue)],
                };
            },
            max: (value: string, maxValue: string) => {
                const valid = Number(value) <= Number(maxValue);

                return {
                    res: value,
                    errors: valid
                        ? null
                        : [validationErrors.number.MAX_VALIDATION_FAILED(value, maxValue)],
                };
            },
            multipleOf: (value: string, multipleOf: string) => {
                const valid = Number(value) % Number(multipleOf) === 0;

                return {
                    res: value,
                    errors: valid
                        ? null
                        : [
                              validationErrors.number.MULTIPLE_OF_VALIDATION_FAILED(
                                  value,
                                  multipleOf
                              ),
                          ],
                };
            },
        },
    },
    enum: {
        validate(
            value: any,
            options: StateArguments,
            resolveParam: (s: string) => string,
            validateDefinition,
            validateValue
        ) {
            const res = value;
            const errors: string[] = [];

            const stringOptions: string[] = [];

            if (options.length) {
                const isOneOf = options.some((option) => {
                    if (typeof option === 'string') {
                        stringOptions.push(option);
                        return resolveParam(option) === value;
                    }
                    return true;
                });
                if (validateValue && !isOneOf) {
                    errors.push(
                        validationErrors.enum.ENUM_TYPE_VALIDATION_FAILED(value, stringOptions)
                    );
                }
            } else if (validateDefinition) {
                errors.push(validationErrors.enum.NO_OPTIONS_DEFINED());
            }

            return { res, errors: errors.length ? errors : null };
        },
    },
};

export function validateRuleStateDefinition(
    selector: string,
    selectorNode: postcss.Rule | postcss.AtRule,
    meta: StylableMeta,
    resolver: StylableResolver,
    diagnostics: Diagnostics
) {
    const selectorAst = parseSelectorWithCache(selector);
    if (selectorAst.length && selectorAst.length === 1) {
        const singleSelectorAst = selectorAst[0];
        const selectorChunk = singleSelectorAst.nodes;
        if (selectorChunk.length === 1 && selectorChunk[0].type === 'class') {
            const className = selectorChunk[0].value;
            const classMeta = CSSClass.get(meta, className);
            const states = classMeta?.[`-st-states`];

            if (states && classMeta._kind === 'class') {
                for (const stateName in states) {
                    // TODO: Sort out types
                    const state = states[stateName];
                    if (state && typeof state === 'object') {
                        const stateParam = isTemplateState(state) ? state.params[0] : state;
                        const { errors } = validateStateArgument(
                            stateParam,
                            meta,
                            stateParam.defaultValue || '',
                            resolver,
                            diagnostics,
                            selectorNode,
                            true,
                            !!stateParam.defaultValue
                        );
                        if (errors && selectorNode.nodes) {
                            for (const node of selectorNode.nodes) {
                                if (node.type === 'decl' && node.prop === `-st-states`) {
                                    diagnostics.report(
                                        stateDiagnostics.DEFAULT_PARAM_FAILS_VALIDATION(
                                            stateName,
                                            stateParam.defaultValue || '',
                                            errors
                                        ),
                                        {
                                            node: node,
                                            word: node.value,
                                        }
                                    );
                                    break;
                                }
                            }
                        }
                    }
                }
            } else {
                // TODO: error state on non-class
            }
        }
    }
}

export function validateStateArgument(
    stateAst: StateParsedValue,
    meta: StylableMeta,
    value: string,
    resolver: StylableResolver,
    diagnostics: Diagnostics,
    selectorNode?: postcss.Node,
    validateDefinition?: boolean,
    validateValue = true
) {
    const resolvedValidations: StateResult = {
        res: resolveParam(
            meta,
            resolver,
            diagnostics,
            selectorNode,
            value || stateAst.defaultValue
        ),
        errors: null,
    };

    const { type: paramType } = stateAst;
    const validator = systemValidators[paramType];

    try {
        if (resolvedValidations.res || validateDefinition) {
            const { errors } = validator.validate(
                resolvedValidations.res,
                stateAst.arguments,
                resolveParam.bind(null, meta, resolver, diagnostics, selectorNode),
                !!validateDefinition,
                validateValue
            );
            resolvedValidations.errors = errors;
        }
    } catch (error) {
        // TODO: warn about validation throwing exception
    }

    return resolvedValidations;
}

// transform

export function transformPseudoClassToCustomState(
    stateDef: MappedStates[string],
    meta: StylableMeta,
    name: string,
    stateNode: PseudoClass,
    namespace: string,
    resolver: StylableResolver,
    diagnostics: Diagnostics,
    selectorNode?: postcss.Node
) {
    if (stateDef === null || typeof stateDef === 'string') {
        if (stateNode.nodes && selectorNode) {
            diagnostics.report(
                stateDiagnostics.NO_PARAM_REQUIRED(name, stringifySelector(stateNode.nodes)),
                {
                    node: selectorNode,
                    word: stringifySelector(stateNode),
                }
            );
        }
        if (stateDef === null) {
            // boolean
            convertToClass(stateNode).value = createBooleanStateClassName(name, namespace);
        } else {
            // static template selector
            // simply concat global mapped selector - ToDo: maybe change to 'selector'
            convertToInvalid(stateNode).value = stateDef;
        }
        delete stateNode.nodes;
    } else if (typeof stateDef === 'object') {
        if (isTemplateState(stateDef)) {
            convertTemplateState(
                meta,
                resolver,
                diagnostics,
                selectorNode,
                stateNode,
                stateDef,
                name
            );
        } else {
            resolveStateValue(
                meta,
                resolver,
                diagnostics,
                selectorNode,
                stateNode,
                stateDef,
                name,
                namespace
            );
        }
    }
}
export function isTemplateState(state: MappedStates[string]): state is TemplateStateParsedValue {
    return !!state && typeof state === 'object' && state.type === 'template';
}

export function createBooleanStateClassName(stateName: string, namespace: string) {
    const escapedNamespace = cssesc(namespace, { isIdentifier: true });
    return `${escapedNamespace}${booleanStateDelimiter}${stateName}`;
}

export function createStateWithParamClassName(stateName: string, namespace: string, param: string) {
    const escapedNamespace = cssesc(namespace, { isIdentifier: true });
    return `${escapedNamespace}${stateWithParamDelimiter}${stateName}${resolveStateParam(
        param,
        true
    )}`;
}

export function resolveStateParam(param: string, escape = false) {
    const result = `${stateMiddleDelimiter}${param.length}${stateMiddleDelimiter}${param.replace(
        /\s/gm,
        '_'
    )}`;
    // adding/removing initial `s` to indicate that it's not the first param of the identifier
    return escape ? cssesc(`s` + result, { isIdentifier: true }).slice(1) : result;
}
function convertTemplateState(
    meta: StylableMeta,
    resolver: StylableResolver,
    diagnostics: Diagnostics,
    selectorNode: postcss.Node | undefined,
    stateNode: PseudoClass,
    stateParamDef: TemplateStateParsedValue,
    name: string
) {
    const paramStateDef = stateParamDef.params[0];
    const resolvedParam = getParamInput(
        meta,
        resolver,
        diagnostics,
        selectorNode,
        stateNode,
        paramStateDef,
        name
    );

    validateParam(meta, resolver, diagnostics, selectorNode, paramStateDef, resolvedParam, name);

    const strippedParam = stripQuotation(resolvedParam);
    transformMappedStateWithParam({
        stateName: name,
        template: stateParamDef.template,
        param: strippedParam,
        node: stateNode,
        selectorNode: selectorNode,
        diagnostics,
    });
}
function getParamInput(
    meta: StylableMeta,
    resolver: StylableResolver,
    diagnostics: Diagnostics,
    selectorNode: postcss.Node | undefined,
    stateNode: PseudoClass,
    stateParamDef: StateParsedValue,
    name: string
) {
    const inputValue =
        stateNode.nodes && stateNode.nodes.length ? stringifySelector(stateNode.nodes) : ``;
    const resolvedParam = resolveParam(
        meta,
        resolver,
        diagnostics,
        selectorNode,
        inputValue ? inputValue : stateParamDef.defaultValue
    );

    if (selectorNode && !inputValue && !stateParamDef.defaultValue) {
        diagnostics.report(stateDiagnostics.NO_STATE_ARGUMENT_GIVEN(name, stateParamDef.type), {
            node: selectorNode,
            word: name,
        });
    }
    return resolvedParam;
}
function validateParam(
    meta: StylableMeta,
    resolver: StylableResolver,
    diagnostics: Diagnostics,
    selectorNode: postcss.Node | undefined,
    stateParamDef: StateParsedValue,
    resolvedParam: string,
    name: string
) {
    const validator = systemValidators[stateParamDef.type];

    let stateParamOutput: StateResult | undefined;
    try {
        stateParamOutput = validator.validate(
            resolvedParam,
            stateParamDef.arguments,
            resolveParam.bind(null, meta, resolver, diagnostics, selectorNode),
            false,
            true
        );
    } catch (e) {
        // TODO: warn about validation throwing exception
    }

    if (stateParamOutput !== undefined) {
        if (stateParamOutput.res !== resolvedParam) {
            resolvedParam = stateParamOutput.res;
        }

        if (selectorNode && stateParamOutput.errors) {
            diagnostics.report(
                stateDiagnostics.FAILED_STATE_VALIDATION(
                    name,
                    resolvedParam,
                    stateParamOutput.errors
                ),
                {
                    node: selectorNode,
                    word: resolvedParam,
                }
            );
        }
    }
}
function resolveStateValue(
    meta: StylableMeta,
    resolver: StylableResolver,
    diagnostics: Diagnostics,
    selectorNode: postcss.Node | undefined,
    stateNode: PseudoClass,
    stateParamDef: StateParsedValue,
    name: string,
    namespace: string
) {
    const resolvedParam = getParamInput(
        meta,
        resolver,
        diagnostics,
        selectorNode,
        stateNode,
        stateParamDef,
        name
    );

    validateParam(meta, resolver, diagnostics, selectorNode, stateParamDef, resolvedParam, name);

    const strippedParam = stripQuotation(resolvedParam);
    convertToClass(stateNode).value = createStateWithParamClassName(name, namespace, strippedParam);
    delete stateNode.nodes;
}

function transformMappedStateWithParam({
    stateName,
    template,
    param,
    node,
    selectorNode,
    diagnostics,
}: {
    stateName: string;
    template: string;
    param: string;
    node: PseudoClass;
    selectorNode?: postcss.Node;
    diagnostics: Diagnostics;
}) {
    const selectorStr = template.replace(/\$0/g, param);
    const selectorAst = parseSelectorWithCache(selectorStr, { clone: true });
    if (
        !validateTemplateSelector({
            stateName,
            selectorStr,
            selectorAst,
            cssNode: selectorNode,
            diagnostics,
        })
    ) {
        return;
    }
    convertToSelector(node).nodes = selectorAst[0].nodes;
}

function validateTemplateSelector({
    stateName,
    selectorStr,
    selectorAst,
    cssNode,
    diagnostics,
}: {
    stateName: string;
    selectorStr: string;
    selectorAst: SelectorList;
    cssNode?: postcss.Node;
    diagnostics: Diagnostics;
}): boolean {
    if (selectorAst.length > 1) {
        if (cssNode) {
            diagnostics.report(
                stateDiagnostics.UNSUPPORTED_MULTI_SELECTOR(stateName, selectorStr),
                {
                    node: cssNode,
                }
            );
        }
        return false;
    } else {
        const firstSelector = selectorAst[0].nodes.find(({ type }) => type !== 'comment');
        if (firstSelector?.type === 'type' || firstSelector?.type === 'universal') {
            if (cssNode) {
                diagnostics.report(
                    stateDiagnostics.UNSUPPORTED_INITIAL_SELECTOR(stateName, selectorStr),
                    {
                        node: cssNode,
                    }
                );
            }
            return false;
        }
        let unexpectedSelector: undefined | SelectorNode = undefined;
        for (const node of selectorAst[0].nodes) {
            if (node.type === 'combinator' || node.type === 'invalid') {
                unexpectedSelector = node;
                break;
            }
        }
        if (unexpectedSelector) {
            if (cssNode) {
                switch (unexpectedSelector.type) {
                    case 'combinator':
                        diagnostics.report(
                            stateDiagnostics.UNSUPPORTED_COMPLEX_SELECTOR(stateName, selectorStr),
                            {
                                node: cssNode,
                            }
                        );
                        break;
                    case 'invalid':
                        diagnostics.report(
                            stateDiagnostics.INVALID_SELECTOR(stateName, selectorStr),
                            {
                                node: cssNode,
                            }
                        );
                        break;
                }
            }
            return false;
        }
    }
    return true;
}

function resolveParam(
    meta: StylableMeta,
    resolver: StylableResolver,
    diagnostics: Diagnostics,
    node?: postcss.Node,
    nodeContent?: string
) {
    const defaultStringValue = '';
    const param = nodeContent || defaultStringValue;
    return evalDeclarationValue(resolver, param, meta, node, undefined, undefined, diagnostics);
}
