import { dirname } from 'path';
import postcss from 'postcss';

import { resolveArgumentsValue } from './functions';
import { cssObjectToAst } from './parser';
import { fixRelativeUrls } from './stylable-assets';
import { ImportSymbol } from './stylable-meta';
import { RefedMixin, SRule, StylableMeta } from './stylable-processor';
import { CSSResolve } from './stylable-resolver';
import { StylableTransformer } from './stylable-transformer';
import { createSubsetAst, isValidDeclaration, mergeRules } from './stylable-utils';
import { valueMapping } from './stylable-value-parsers';

export const mixinWarnings = {
    FAILED_TO_APPLY_MIXIN(error: string) {
        return `could not apply mixin: ${error}`;
    },
    JS_MIXIN_NOT_A_FUNC() {
        return `js mixin must be a function`;
    },
    CIRCULAR_MIXIN(circularPaths: string[]) {
        return `circular mixin found: ${circularPaths.join(' --> ')}`;
    },
    UNKNOWN_MIXIN_SYMBOL(name: string) {
        return `cannot mixin unknown symbol "${name}"`;
    },
};

export function appendMixins(
    transformer: StylableTransformer,
    rule: SRule,
    meta: StylableMeta,
    variableOverride: Record<string, string>,
    cssVarsMapping: Record<string, string>,
    path: string[] = []
) {
    if (!rule.mixins || rule.mixins.length === 0) {
        return;
    }
    rule.mixins.forEach((mix) => {
        appendMixin(mix, transformer, rule, meta, variableOverride, cssVarsMapping, path);
    });
    rule.mixins.length = 0;
    rule.walkDecls(valueMapping.mixin, (node) => node.remove());
}

export function appendMixin(
    mix: RefedMixin,
    transformer: StylableTransformer,
    rule: SRule,
    meta: StylableMeta,
    variableOverride: Record<string, string>,
    cssVarsMapping: Record<string, string>,
    path: string[] = []
) {
    if (checkRecursive(transformer, meta, mix, rule, path)) {
        return;
    }

    const local = meta.mappedSymbols[mix.mixin.type];
    if (local && (local._kind === 'class' || local._kind === 'element')) {
        handleLocalClassMixin(mix, transformer, meta, variableOverride, cssVarsMapping, path, rule);
    } else {
        const resolvedMixin = transformer.resolver.resolve(mix.ref);
        if (resolvedMixin) {
            if (resolvedMixin._kind === 'js') {
                if (typeof resolvedMixin.symbol === 'function') {
                    try {
                        handleJSMixin(
                            transformer,
                            mix,
                            resolvedMixin.symbol,
                            meta,
                            rule,
                            variableOverride
                        );
                    } catch (e) {
                        transformer.diagnostics.error(
                            rule,
                            mixinWarnings.FAILED_TO_APPLY_MIXIN(e),
                            { word: mix.mixin.type }
                        );
                        return;
                    }
                } else {
                    transformer.diagnostics.error(rule, mixinWarnings.JS_MIXIN_NOT_A_FUNC(), {
                        word: mix.mixin.type,
                    });
                }
            } else {
                handleImportedCSSMixin(
                    transformer,
                    mix,
                    rule,
                    meta,
                    path,
                    variableOverride,
                    cssVarsMapping
                );
            }
        } else {
            // TODO: error cannot resolve mixin
        }
    }
}

function checkRecursive(
    transformer: StylableTransformer,
    meta: StylableMeta,
    mix: RefedMixin,
    rule: postcss.Rule,
    path: string[]
) {
    const symbolName =
        mix.ref.name === meta.root
            ? mix.ref._kind === 'class'
                ? meta.root
                : 'default'
            : mix.mixin.type;
    const isRecursive = path.includes(symbolName + ' from ' + meta.source);
    if (isRecursive) {
        // Todo: add test verifying word
        transformer.diagnostics.warn(rule, mixinWarnings.CIRCULAR_MIXIN(path), {
            word: symbolName,
        });
        return true;
    }
    return false;
}

function handleJSMixin(
    transformer: StylableTransformer,
    mix: RefedMixin,
    mixinFunction: (...args: any[]) => any,
    meta: StylableMeta,
    rule: postcss.Rule,
    variableOverride?: Record<string, string>
) {
    const res = mixinFunction((mix.mixin.options as any[]).map((v) => v.value));
    const mixinRoot = cssObjectToAst(res).root;

    mixinRoot.walkDecls((decl) => {
        if (!isValidDeclaration(decl)) {
            decl.value = String(decl);
        }
    });

    transformer.transformAst(mixinRoot, meta, undefined, variableOverride, [], true);

    const mixinPath = (mix.ref as ImportSymbol).import.from;
    fixRelativeUrls(
        mixinRoot,
        transformer.fileProcessor.resolvePath(mixinPath, dirname(meta.source)),
        meta.source
    );

    mergeRules(mixinRoot, rule);
}

function createMixinRootFromCSSResolve(
    transformer: StylableTransformer,
    mix: RefedMixin,
    meta: StylableMeta,
    resolvedClass: CSSResolve,
    path: string[],
    decl: postcss.Declaration,
    variableOverride: Record<string, string>,
    cssVarsMapping: Record<string, string>
) {
    const isRootMixin = resolvedClass.symbol.name === resolvedClass.meta.root;
    const mixinRoot = createSubsetAst<postcss.Root>(
        resolvedClass.meta.ast,
        (resolvedClass.symbol._kind === 'class' ? '.' : '') + resolvedClass.symbol.name,
        undefined,
        isRootMixin
    );

    const namedArgs = mix.mixin.options as Record<string, string>;
    const resolvedArgs = resolveArgumentsValue(
        namedArgs,
        transformer,
        meta,
        transformer.diagnostics,
        decl,
        variableOverride,
        path,
        cssVarsMapping
    );

    const mixinMeta: StylableMeta = isRootMixin
        ? resolvedClass.meta
        : createInheritedMeta(resolvedClass);
    const symbolName = isRootMixin ? 'default' : mix.mixin.type;

    transformer.transformAst(
        mixinRoot,
        mixinMeta,
        undefined,
        resolvedArgs,
        path.concat(symbolName + ' from ' + meta.source),
        true
    );

    fixRelativeUrls(mixinRoot, mixinMeta.source, meta.source);

    return mixinRoot;
}

function handleImportedCSSMixin(
    transformer: StylableTransformer,
    mix: RefedMixin,
    rule: postcss.Rule,
    meta: StylableMeta,
    path: string[],
    variableOverride: Record<string, string>,
    cssVarsMapping: Record<string, string>
) {
    let resolvedClass = transformer.resolver.resolve(mix.ref) as CSSResolve;
    const roots = [];

    while (resolvedClass && resolvedClass.symbol && resolvedClass._kind === 'css') {
        const mixinDecl = getMixinDeclaration(rule) || postcss.decl();
        roots.push(
            createMixinRootFromCSSResolve(
                transformer,
                mix,
                meta,
                resolvedClass,
                path,
                mixinDecl,
                variableOverride,
                cssVarsMapping
            )
        );
        if (
            (resolvedClass.symbol._kind === 'class' || resolvedClass.symbol._kind === 'element') &&
            !resolvedClass.symbol[valueMapping.extends]
        ) {
            resolvedClass = transformer.resolver.resolve(resolvedClass.symbol) as CSSResolve;
        } else {
            break;
        }
    }
    if (roots.length === 1) {
        mergeRules(roots[0], rule);
    } else if (roots.length > 1) {
        const mixinRoot = postcss.root();
        roots.forEach((root) => mixinRoot.prepend(...root.nodes!));
        mergeRules(mixinRoot, rule);
    } else {
        const mixinDecl = getMixinDeclaration(rule);

        if (mixinDecl) {
            transformer.diagnostics.error(
                mixinDecl,
                mixinWarnings.UNKNOWN_MIXIN_SYMBOL(mixinDecl.value),
                { word: mixinDecl.value }
            );
        }
    }
}

function handleLocalClassMixin(
    mix: RefedMixin,
    transformer: StylableTransformer,
    meta: StylableMeta,
    variableOverride: ({ [key: string]: string } & object) | undefined,
    cssVarsMapping: Record<string, string>,
    path: string[],
    rule: SRule
) {
    const isRootMixin = mix.ref.name === meta.root;
    const namedArgs = mix.mixin.options as Record<string, string>;
    const mixinDecl = getMixinDeclaration(rule) || postcss.decl();
    const resolvedArgs = resolveArgumentsValue(
        namedArgs,
        transformer,
        meta,
        transformer.diagnostics,
        mixinDecl,
        variableOverride,
        path,
        cssVarsMapping
    );

    const mixinRoot = createSubsetAst<postcss.Root>(
        meta.ast,
        '.' + mix.ref.name,
        undefined,
        isRootMixin
    );

    transformer.transformAst(
        mixinRoot,
        isRootMixin ? meta : createInheritedMeta({ meta, symbol: mix.ref, _kind: 'css' }),
        undefined,
        resolvedArgs,
        path.concat(mix.mixin.type + ' from ' + meta.source),
        true
    );
    mergeRules(mixinRoot, rule);
}

function createInheritedMeta(resolvedClass: CSSResolve) {
    const mixinMeta: StylableMeta = Object.create(resolvedClass.meta);
    mixinMeta.parent = resolvedClass.meta;
    mixinMeta.mappedSymbols = Object.create(resolvedClass.meta.mappedSymbols);
    mixinMeta.mappedSymbols[resolvedClass.meta.root] =
        resolvedClass.meta.mappedSymbols[resolvedClass.symbol.name];
    return mixinMeta;
}

function getMixinDeclaration(rule: postcss.Rule): postcss.Declaration | undefined {
    return (
        rule.nodes &&
        (rule.nodes.find((node) => {
            return node.type === 'decl' && node.prop === valueMapping.mixin;
        }) as postcss.Declaration)
    );
}
