import { createFeature, FeatureContext, FeatureTransformContext } from './feature';
import { generalDiagnostics } from './diagnostics';
import * as STSymbol from './st-symbol';
import type { StylableSymbol } from './st-symbol';
import type { ImportSymbol } from './st-import';
import type { ElementSymbol } from './css-type';
import type * as STStructure from './st-structure';
import * as STCustomState from './st-custom-state';
import { getOriginDefinition } from '../helpers/resolve';
import { namespace } from '../helpers/namespace';
import { namespaceEscape, unescapeCSS } from '../helpers/escape';
import { getNamedArgs } from '../helpers/value';
import {
    convertToClass,
    stringifySelector,
    isSimpleSelector,
    parseSelectorWithCache,
    convertToPseudoClass,
    convertToSelector,
} from '../helpers/selector';
import { getAlias } from '../stylable-utils';
import type { StylableMeta } from '../stylable-meta';
import { validateRuleStateDefinition } from '../helpers/custom-state';
import type { Stylable } from '../stylable';
import {
    ImmutableClass,
    Class,
    SelectorNode,
    ImmutableSelectorNode,
    stringifySelectorAst,
    SelectorNodes,
} from '@tokey/css-selector-parser';
import * as postcss from 'postcss';
import { basename } from 'path';
import { createDiagnosticReporter } from '../diagnostics';
import postcssValueParser from 'postcss-value-parser';
import { plugableRecord } from '../helpers/plugable-record';

export interface StPartDirectives extends STStructure.HasParts, Partial<STCustomState.HasStates> {
    '-st-root'?: boolean;
    '-st-extends'?: ImportSymbol | ClassSymbol | ElementSymbol;
    '-st-global'?: SelectorNode[];
}

const stPartDirectives = {
    '-st-root': true,
    '-st-states': true,
    '-st-extends': true,
    '-st-global': true,
} as const;

export interface ClassSymbol extends StPartDirectives {
    _kind: 'class';
    name: string;
    alias?: ImportSymbol;
    scoped?: string; // ToDo: check if in use
}

export const diagnostics = {
    INVALID_FUNCTIONAL_SELECTOR: generalDiagnostics.INVALID_FUNCTIONAL_SELECTOR,
    UNSCOPED_CLASS: createDiagnosticReporter(
        '00002',
        'warning',
        (name: string) =>
            `unscoped class "${name}" will affect all elements of the same type in the document`
    ),
    STATE_DEFINITION_IN_ELEMENT: createDiagnosticReporter(
        '11002',
        'error',
        () => 'cannot define pseudo states inside a type selector'
    ),
    STATE_DEFINITION_IN_COMPLEX: createDiagnosticReporter(
        '11003',
        'error',
        () => 'cannot define pseudo states inside complex selectors'
    ),
    OVERRIDE_TYPED_RULE: createDiagnosticReporter(
        '11006',
        'warning',
        (key: string, name: string) => `override "${key}" on typed rule "${name}"`
    ),
    CANNOT_RESOLVE_EXTEND: createDiagnosticReporter(
        '11004',
        'error',
        (name: string) => `cannot resolve '-st-extends' type for '${name}'`
    ),
    CANNOT_EXTEND_IN_COMPLEX: createDiagnosticReporter(
        '11005',
        'error',
        () => `cannot define "-st-extends" inside a complex selector`
    ),
    EMPTY_ST_GLOBAL: createDiagnosticReporter(
        '00003',
        'error',
        () => `-st-global must contain a valid selector`
    ),
    UNSUPPORTED_MULTI_SELECTORS_ST_GLOBAL: createDiagnosticReporter(
        '00004',
        'error',
        () => `unsupported multi selector in -st-global`
    ),
    UNSUPPORTED_COMPLEX_SELECTOR: createDiagnosticReporter(
        '00010',
        'error',
        () => `unsupported complex selector`
    ),
    IMPORT_ISNT_EXTENDABLE: createDiagnosticReporter(
        '00005',
        'error',
        () => 'import is not extendable'
    ),
    CANNOT_EXTEND_UNKNOWN_SYMBOL: createDiagnosticReporter(
        '00006',
        'error',
        (name: string) => `cannot extend unknown symbol "${name}"`
    ),
    CANNOT_EXTEND_JS: createDiagnosticReporter(
        '00007',
        'error',
        () => 'JS import is not extendable'
    ),
    UNKNOWN_IMPORT_ALIAS: createDiagnosticReporter(
        '00008',
        'error',
        (name: string) => `cannot use alias for unknown import "${name}"`
    ),
    DISABLED_DIRECTIVE: createDiagnosticReporter(
        '00009',
        'error',
        (className: string, directive: keyof typeof stPartDirectives) => {
            const alternative =
                directive === '-st-extends'
                    ? ` use "@st .${className} :is(.base)" instead`
                    : directive === '-st-global'
                    ? `use "@st .${className} => :global(<selector>)" instead`
                    : directive === '-st-states'
                    ? `use "@st .${className} { @st .state; }" instead`
                    : '';
            return `cannot use ${directive} on .${className} since class is defined with "@st" - ${alternative}`;
        }
    ),
};

const dataKey = plugableRecord.key<{
    classesDefinedWithAtSt: Set<string>;
}>('st-structure');

// HOOKS

export const hooks = createFeature<{
    SELECTOR: Class;
    IMMUTABLE_SELECTOR: ImmutableClass;
    RESOLVED: Record<string, { classes: string; isLocal: boolean }>;
}>({
    metaInit({ meta }) {
        plugableRecord.set(meta.data, dataKey, {
            classesDefinedWithAtSt: new Set<string>(),
        });
    },
    analyzeSelectorNode({ context, node, rule }): void {
        if (node.nodes) {
            // error on functional class
            context.diagnostics.report(
                diagnostics.INVALID_FUNCTIONAL_SELECTOR(`.` + node.value, `class`),
                {
                    node: rule,
                    word: stringifySelector(node),
                }
            );
        }
        addClass(context, node.value, rule);
    },
    analyzeDeclaration({ context, decl }) {
        if (context.meta.type === 'stylable' && isDirectiveDeclaration(decl)) {
            handleDirectives(context, decl);
        }
    },
    transformResolve({ context }) {
        const resolvedSymbols = context.getResolvedSymbols(context.meta);
        const locals: Record<string, { classes: string; isLocal: boolean }> = {};
        for (const [localName, resolved] of Object.entries(resolvedSymbols.class)) {
            const exportedClasses = [];
            let first = true;
            // collect list of css classes for exports
            for (const { meta, symbol } of resolved) {
                if (!first && symbol[`-st-root`]) {
                    // extended stylesheet root: stop collection as root is expected to
                    // be placed by inner component, for example in <Button class={classes.primaryBtn} />
                    // `primaryBtn` shouldn't contain `button__root` as it is placed by the Button component
                    break;
                }
                first = false;
                if (symbol[`-st-global`]) {
                    // collect global override just in case of
                    // compound set of CSS classes
                    let isOnlyClasses = true;
                    const globalClasses = symbol[`-st-global`].reduce<string[]>(
                        (globalClasses, node) => {
                            if (node.type === `class`) {
                                globalClasses.push(node.value);
                                context.meta.globals[node.value] = true;
                            } else {
                                isOnlyClasses = false;
                            }
                            return globalClasses;
                        },
                        []
                    );
                    if (isOnlyClasses) {
                        exportedClasses.push(...globalClasses);
                    }
                    continue;
                }
                if (symbol.alias && !symbol[`-st-extends`]) {
                    continue;
                }
                exportedClasses.push(namespace(symbol.name, meta.namespace));
            }
            const classNames = unescapeCSS(exportedClasses.join(' '));
            if (classNames) {
                const directResolve = resolved[0];
                const isLocal = directResolve.meta === context.meta && !directResolve.symbol.alias;
                locals[localName] = { classes: classNames, isLocal };
            }
        }
        return locals;
    },
    transformSelectorNode({ context, selectorContext, node }) {
        const { originMeta, resolver } = selectorContext;
        const resolvedSymbols = context.getResolvedSymbols(context.meta);
        const resolved = resolvedSymbols.class[node.value] || [
            // used to namespace classes from js mixins since js mixins
            // are scoped in the context of the mixed-in stylesheet
            // which might not have a definition for the mixed-in class
            { _kind: 'css', meta: originMeta, symbol: createSymbol({ name: node.value }) },
        ];
        selectorContext.setNextSelectorScope(resolved, node, node.value);
        const { symbol, meta } = getOriginDefinition(resolved);
        if (selectorContext.originMeta === meta && symbol[`-st-states`]) {
            // ToDo: refactor out to transformer validation phase
            validateRuleStateDefinition(
                selectorContext.selectorStr,
                selectorContext.ruleOrAtRule,
                context.meta,
                resolver,
                context.diagnostics
            );
        }
        if (selectorContext.transform) {
            namespaceClass(meta, symbol, node);
        }
    },
    transformJSExports({ exports, resolved }) {
        for (const [localName, { classes, isLocal }] of Object.entries(resolved)) {
            if (isLocal) {
                exports.classes[localName] = classes;
            }
        }
    },
});

// API

export class StylablePublicApi {
    constructor(private stylable: Stylable) {}
    public transformIntoSelector(meta: StylableMeta, name: string): string | undefined {
        const localSymbol = STSymbol.get(meta, name);
        const resolved =
            localSymbol?._kind === 'import'
                ? this.stylable.resolver.deepResolve(localSymbol)
                : { _kind: 'css', meta, symbol: localSymbol };

        if (resolved?._kind !== 'css' || resolved.symbol?._kind !== 'class') {
            return undefined;
        }

        const node: Class = {
            type: 'class',
            value: '',
            start: 0,
            end: 0,
            dotComments: [],
        };
        namespaceClass(resolved.meta, resolved.symbol, node, false);
        return stringifySelectorAst(node);
    }
}

export function get(meta: StylableMeta, name: string): ClassSymbol | undefined {
    return STSymbol.get(meta, name, `class`);
}

export function getAll(meta: StylableMeta): Record<string, ClassSymbol> {
    return STSymbol.getAllByType(meta, `class`);
}

export function createSymbol(input: Partial<ClassSymbol> & { name: string }): ClassSymbol {
    const parts = input['-st-parts'] || {};
    return { ...input, _kind: 'class', '-st-parts': parts };
}

export function addClass(context: FeatureContext, name: string, rule?: postcss.Node): ClassSymbol {
    let symbol = STSymbol.get(context.meta, name, `class`);
    if (!symbol) {
        let alias = STSymbol.get(context.meta, name);
        if (alias && alias._kind !== 'import') {
            alias = undefined;
        }
        symbol = STSymbol.addSymbol({
            context,
            symbol: createSymbol({ name, alias }),
            node: rule,
            safeRedeclare: !!alias,
        }) as ClassSymbol;
    }
    // mark native css as global
    if (context.meta.type === 'css' && !symbol['-st-global']) {
        symbol['-st-global'] = [
            {
                type: 'class',
                value: name,
                dotComments: [],
                start: 0,
                end: 0,
            },
        ];
    }
    return symbol;
}

export function namespaceClass(
    meta: StylableMeta,
    symbol: StylableSymbol,
    node: SelectorNode, // ToDo: check this is the correct type, should this be inline selector?
    wrapInGlobal = true
) {
    if (`-st-global` in symbol && symbol[`-st-global`]) {
        // change node to `-st-global` value
        if (wrapInGlobal) {
            const globalMappedNodes = symbol[`-st-global`];
            convertToPseudoClass(node, 'global', [
                {
                    type: 'selector',
                    nodes: globalMappedNodes,
                    after: '',
                    before: '',
                    end: 0,
                    start: 0,
                },
            ]);
        } else {
            const flatNode = convertToSelector(node);
            const globalMappedNodes = symbol[`-st-global`];
            flatNode.nodes = globalMappedNodes;
        }
    } else {
        node = convertToClass(node);
        node.value = namespaceEscape(symbol.name, meta.namespace);
    }
}
function getNamespacedClass(meta: StylableMeta, symbol: StylableSymbol) {
    if (`-st-global` in symbol && symbol[`-st-global`]) {
        const selector = symbol[`-st-global`];
        return stringifySelectorAst(selector as any);
    } else {
        return '.' + namespaceEscape(symbol.name, meta.namespace);
    }
}

export function addDevRules({ getResolvedSymbols, meta }: FeatureTransformContext) {
    const resolvedSymbols = getResolvedSymbols(meta);
    for (const resolved of Object.values(resolvedSymbols.class)) {
        const a = resolved[0];
        if (resolved.length > 1 && a.symbol['-st-extends']) {
            const b = resolved[resolved.length - 1];
            meta.targetAst!.append(
                createWarningRule(
                    '.' + b.symbol.name,
                    getNamespacedClass(b.meta, b.symbol),
                    basename(b.meta.source),
                    '.' + a.symbol.name,
                    getNamespacedClass(a.meta, a.symbol),
                    basename(a.meta.source)
                )
            );
        }
    }
}

export function createWarningRule(
    extendedNode: string,
    scopedExtendedNode: string,
    extendedFile: string,
    extendingNode: string,
    scopedExtendingNode: string,
    extendingFile: string
) {
    const message = `"class extending component '${extendingNode} => ${scopedExtendingNode}' in stylesheet '${extendingFile}' was set on a node that does not extend '${extendedNode} => ${scopedExtendedNode}' from stylesheet '${extendedFile}'" !important`;
    return postcss.rule({
        raws: { between: ' ' },
        selector: `${scopedExtendingNode}:not(${scopedExtendedNode})::before`,
        nodes: [
            postcss.decl({
                prop: 'content',
                value: message,
            }),
            postcss.decl({
                prop: 'display',
                value: `block !important`,
            }),
            postcss.decl({
                prop: 'font-family',
                value: `monospace !important`,
            }),
            postcss.decl({
                prop: 'background-color',
                value: `red !important`,
            }),
            postcss.decl({
                prop: 'color',
                value: `white !important`,
            }),
        ],
    });
}

export function validateClassScoping({
    context,
    classSymbol,
    locallyScoped,
    reportUnscoped,
    node,
    nodes,
    index,
    rule,
}: {
    context: FeatureContext;
    classSymbol: ClassSymbol;
    locallyScoped: boolean;
    reportUnscoped: boolean;
    node: ImmutableClass;
    nodes: ImmutableSelectorNode[];
    index: number;
    rule: postcss.Rule;
}): boolean {
    if (context.meta.type !== 'stylable') {
        // ignore in native CSS
        return true;
    }
    if (!classSymbol.alias) {
        return true;
    } else if (locallyScoped === false) {
        if (checkForScopedNodeAfter(context, rule, nodes, index) === false) {
            if (reportUnscoped) {
                context.diagnostics.report(diagnostics.UNSCOPED_CLASS(node.value), {
                    node: rule,
                    word: node.value,
                });
            }
            return false;
        } else {
            return true;
        }
    }
    return locallyScoped;
}

// ToDo: support more complex cases (e.g. `:is`)
export function checkForScopedNodeAfter(
    context: FeatureContext,
    rule: postcss.Rule,
    nodes: ImmutableSelectorNode[],
    index: number
) {
    for (let i = index + 1; i < nodes.length; i++) {
        const node = nodes[i];
        if (!node) {
            // ToDo: can this get here???
            break;
        }
        if (node.type === 'combinator') {
            break;
        }
        if (node.type === 'class') {
            const name = node.value;
            const classSymbol = addClass(context, name, rule);
            if (classSymbol && !classSymbol.alias) {
                return true;
            }
        }
    }
    return false;
}

function isDirectiveDeclaration(
    decl: postcss.Declaration
): decl is postcss.Declaration & { prop: keyof typeof stPartDirectives } {
    return decl.prop in stPartDirectives;
}
export function disableDirectivesForClass(context: FeatureContext, className: string) {
    // ToDo: move directive analyze to @st-structure
    // called when class is defined with @st
    const { classesDefinedWithAtSt } = plugableRecord.getUnsafe(context.meta.data, dataKey);
    classesDefinedWithAtSt.add(className);
}

function handleDirectives(
    context: FeatureContext,
    decl: postcss.Declaration & { prop: keyof typeof stPartDirectives }
) {
    const rule = decl.parent as postcss.Rule;
    if (rule?.type !== 'rule') {
        return;
    }
    const isSimplePerSelector = isSimpleSelector(rule.selector);
    const type = isSimplePerSelector.reduce((accType, { type }) => {
        return !accType ? type : accType !== type ? `complex` : type;
    }, `` as (typeof isSimplePerSelector)[number]['type']);
    const isSimple = type !== `complex`;

    const { classesDefinedWithAtSt } = plugableRecord.getUnsafe(context.meta.data, dataKey);
    if (type === 'class' && classesDefinedWithAtSt.has(rule.selector.replace('.', ''))) {
        context.diagnostics.report(
            diagnostics.DISABLED_DIRECTIVE(rule.selector.replace('.', ''), decl.prop),
            {
                node: decl,
            }
        );
        return;
    } else if (decl.prop === `-st-states`) {
        if (isSimple && type !== 'type') {
            extendTypedRule(
                context,
                decl,
                rule.selector,
                `-st-states`,
                STCustomState.parsePseudoStates(decl.value, decl, context.diagnostics)
            );
        } else {
            if (type === 'type') {
                context.diagnostics.report(diagnostics.STATE_DEFINITION_IN_ELEMENT(), {
                    node: decl,
                });
            } else {
                context.diagnostics.report(diagnostics.STATE_DEFINITION_IN_COMPLEX(), {
                    node: decl,
                });
            }
        }
    } else if (decl.prop === `-st-extends`) {
        if (isSimple) {
            const parsed = parseStExtends(decl.value);
            const symbolName = parsed.types[0] && parsed.types[0].symbolName;

            const extendsRefSymbol = STSymbol.get(context.meta, symbolName)!;
            if (
                (extendsRefSymbol &&
                    (extendsRefSymbol._kind === 'import' ||
                        extendsRefSymbol._kind === 'class' ||
                        extendsRefSymbol._kind === 'element')) ||
                decl.value === context.meta.root
            ) {
                extendTypedRule(
                    context,
                    decl,
                    rule.selector,
                    `-st-extends`,
                    getAlias(extendsRefSymbol) || extendsRefSymbol
                );
            } else {
                context.diagnostics.report(diagnostics.CANNOT_RESOLVE_EXTEND(decl.value), {
                    node: decl,
                    word: decl.value,
                });
            }
        } else {
            context.diagnostics.report(diagnostics.CANNOT_EXTEND_IN_COMPLEX(), {
                node: decl,
            });
        }
    } else if (decl.prop === `-st-global`) {
        if (isSimple && type !== 'type') {
            // set class global mapping
            const name = rule.selector.replace('.', '');
            const classSymbol = get(context.meta, name);
            if (classSymbol) {
                const globalSelectorAst = parseStGlobal(context, decl);
                if (globalSelectorAst) {
                    classSymbol[`-st-global`] = globalSelectorAst;
                }
            }
        } else {
            // TODO: diagnostics - scoped on none class
        }
    }
}

export function extendTypedRule(
    context: FeatureContext,
    node: postcss.Node,
    selector: string,
    key: keyof StPartDirectives,
    value: any
) {
    const name = selector.replace('.', '');
    const typedRule = STSymbol.get(context.meta, name) as ClassSymbol | ElementSymbol;
    if (typedRule && typedRule[key]) {
        context.diagnostics.report(diagnostics.OVERRIDE_TYPED_RULE(key, name), {
            node,
            word: name,
        });
    }
    if (typedRule) {
        typedRule[key] = value;
    }
}

export interface ArgValue {
    type: string;
    value: string;
}
export interface ExtendsValue {
    symbolName: string;
    args: ArgValue[][] | null;
}

export function parseStExtends(value: string) {
    const ast = postcssValueParser(value);
    const types: ExtendsValue[] = [];

    ast.walk((node) => {
        if (node.type === 'function') {
            const args = getNamedArgs(node);

            types.push({
                symbolName: node.value,
                args,
            });

            return false;
        } else if (node.type === 'word') {
            types.push({
                symbolName: node.value,
                args: null,
            });
        }
        return undefined;
    }, false);

    return {
        ast,
        types,
    };
}
function parseStGlobal(
    context: FeatureContext,
    decl: postcss.Declaration
): SelectorNodes | undefined {
    const selector = parseSelectorWithCache(decl.value.replace(/^['"]/, '').replace(/['"]$/, ''), {
        clone: true,
    });
    if (!selector[0]) {
        context.diagnostics.report(diagnostics.EMPTY_ST_GLOBAL(), {
            node: decl,
        });
        return;
    } else if (selector.length > 1) {
        context.diagnostics.report(diagnostics.UNSUPPORTED_MULTI_SELECTORS_ST_GLOBAL(), {
            node: decl,
        });
        return;
    } else {
        for (const node of selector[0].nodes) {
            if (node.type === 'combinator') {
                context.diagnostics.report(diagnostics.UNSUPPORTED_COMPLEX_SELECTOR(), {
                    node: decl,
                });
                return;
            }
        }
    }
    return selector[0].nodes;
}
