import { dirname, relative } from 'path';
import postcssValueParser from 'postcss-value-parser';
import type * as postcss from 'postcss';
import { createDiagnosticReporter, Diagnostics } from './diagnostics';
import { nativeFunctionsDic } from './native-reserved-lists';
import { assureRelativeUrlPrefix } from './stylable-assets';
import type { StylableMeta } from './stylable-meta';
import {
    StylableResolver,
    createSymbolResolverWithCache,
    MetaResolvedSymbols,
} from './stylable-resolver';
import type { replaceValueHook, RuntimeStVar, StylableTransformer } from './stylable-transformer';
import { getFormatterArgs, getStringValue, stringifyFunction } from './helpers/value';
import { unescapeCSS } from './helpers/escape';
import type { ParsedValue } from './types';
import type { FeatureTransformContext } from './features/feature';
import { CSSCustomProperty, STVar } from './features';
import { unbox, CustomValueError } from './custom-values';

export interface EvalValueData {
    value: string;
    node?: postcss.Node;
    meta: StylableMeta;
    stVarOverride?: Record<string, string> | null;
    args?: string[];
    rootArgument?: string;
    initialNode?: postcss.Node;
}

export interface EvalValueResult {
    topLevelType: any;
    runtimeValue: RuntimeStVar;
    outputValue: string;
    typeError?: Error;
}

export class StylableEvaluator {
    public stVarOverride: Record<string, string> | null | undefined;
    public getResolvedSymbols: (meta: StylableMeta) => MetaResolvedSymbols;
    public valueHook?: replaceValueHook;
    constructor(options: {
        stVarOverride?: Record<string, string> | null;
        valueHook?: replaceValueHook;
        getResolvedSymbols: (meta: StylableMeta) => MetaResolvedSymbols;
    }) {
        this.valueHook = options.valueHook;
        this.stVarOverride = options.stVarOverride;
        this.getResolvedSymbols = options.getResolvedSymbols;
    }
    evaluateValue(
        context: Omit<FeatureTransformContext, 'getResolvedSymbols'>,
        data: Omit<EvalValueData, 'passedThrough' | 'valueHook'>
    ) {
        return processDeclarationValue(
            context.resolver,
            this.getResolvedSymbols,
            data.value,
            data.meta,
            data.node,
            data.stVarOverride || this.stVarOverride,
            this.valueHook,
            context.diagnostics,
            context.passedThrough,
            data.args,
            data.rootArgument,
            data.initialNode
        );
    }
}

// old API

export const functionDiagnostics = {
    FAIL_TO_EXECUTE_FORMATTER: createDiagnosticReporter(
        '15001',
        'error',
        (resolvedValue: string, message: string) =>
            `failed to execute formatter "${resolvedValue}" with error: "${message}"`
    ),
    UNKNOWN_FORMATTER: createDiagnosticReporter(
        '15002',
        'error',
        (name: string) => `cannot find native function or custom formatter called ${name}`
    ),
};

export function resolveArgumentsValue(
    options: Record<string, string>,
    transformer: StylableTransformer,
    meta: StylableMeta,
    diagnostics: Diagnostics,
    node: postcss.Node,
    variableOverride?: Record<string, string>,
    path?: string[]
) {
    const resolvedArgs = {} as Record<string, string>;
    for (const k in options) {
        resolvedArgs[k] = evalDeclarationValue(
            transformer.resolver,
            unescapeCSS(options[k]),
            meta,
            node,
            variableOverride,
            transformer.replaceValueHook,
            diagnostics,
            path,
            undefined
        );
    }
    return resolvedArgs;
}

export function processDeclarationValue(
    resolver: StylableResolver,
    getResolvedSymbols: (meta: StylableMeta) => MetaResolvedSymbols,
    value: string,
    meta: StylableMeta,
    node?: postcss.Node,
    variableOverride?: Record<string, string> | null,
    valueHook?: replaceValueHook,
    diagnostics: Diagnostics = new Diagnostics(),
    passedThrough: string[] = [],
    args: string[] = [],
    rootArgument?: string,
    initialNode?: postcss.Node
): EvalValueResult {
    const evaluator = new StylableEvaluator({
        stVarOverride: variableOverride,
        valueHook,
        getResolvedSymbols,
    });
    const resolvedSymbols = getResolvedSymbols(meta);
    const parsedValue: any = postcssValueParser(value);
    parsedValue.walk((parsedNode: ParsedValue) => {
        const { type, value } = parsedNode;
        if (type === `function`) {
            if (value === 'value') {
                STVar.hooks.transformValue({
                    context: {
                        meta,
                        diagnostics,
                        resolver,
                        evaluator,
                        getResolvedSymbols,
                        passedThrough,
                    },
                    data: {
                        value,
                        node,
                        meta,
                        stVarOverride: variableOverride,
                        args,
                        rootArgument,
                        initialNode,
                    },
                    node: parsedNode,
                });
            } else if (value === '') {
                parsedNode.resolvedValue = stringifyFunction(value, parsedNode);
            } else if (resolvedSymbols.customValues[value]) {
                // no op resolved at the bottom
            } else if (value === 'url') {
                // postcss-value-parser treats url differently:
                // https://github.com/TrySound/postcss-value-parser/issues/34
                const url = parsedNode.nodes[0];
                if ((url.type === 'word' || url.type === 'string') && url.value.startsWith('~')) {
                    const sourceDir = dirname(meta.source);
                    url.value = assureRelativeUrlPrefix(
                        relative(
                            sourceDir,
                            resolver.resolvePath(sourceDir, url.value.slice(1))
                        ).replace(/\\/gm, '/')
                    );
                }
            } else if (value === 'format') {
                // preserve native format function arg quotation
                parsedNode.resolvedValue = stringifyFunction(value, parsedNode, true);
            } else if (resolvedSymbols.js[value]) {
                const formatter = resolvedSymbols.js[value];
                const formatterArgs = getFormatterArgs(parsedNode);
                try {
                    // ToDo: check if function instead of calling on a non function
                    parsedNode.resolvedValue = (formatter.symbol as (...args: any[]) => any)(
                        ...formatterArgs
                    );
                    if (evaluator.valueHook && typeof parsedNode.resolvedValue === 'string') {
                        parsedNode.resolvedValue = evaluator.valueHook(
                            parsedNode.resolvedValue,
                            { name: parsedNode.value, args: formatterArgs },
                            true,
                            passedThrough
                        );
                    }
                } catch (error) {
                    parsedNode.resolvedValue = stringifyFunction(value, parsedNode);
                    if (diagnostics && node) {
                        diagnostics.report(
                            functionDiagnostics.FAIL_TO_EXECUTE_FORMATTER(
                                parsedNode.resolvedValue,
                                (error as Error)?.message
                            ),
                            {
                                node,
                                word: (node as postcss.Declaration).value,
                            }
                        );
                    }
                }
            } else if (value === 'var') {
                CSSCustomProperty.hooks.transformValue({
                    context: {
                        meta,
                        diagnostics,
                        resolver,
                        evaluator,
                        getResolvedSymbols,
                        passedThrough,
                    },
                    data: {
                        value,
                        node,
                        meta,
                        stVarOverride: variableOverride,
                        args,
                        rootArgument,
                        initialNode,
                    },
                    node: parsedNode,
                });
            } else if (nativeFunctionsDic[value]) {
                const { preserveQuotes } = nativeFunctionsDic[value];
                parsedNode.resolvedValue = stringifyFunction(value, parsedNode, preserveQuotes);
            } else if (node) {
                parsedNode.resolvedValue = stringifyFunction(value, parsedNode);
                diagnostics.report(functionDiagnostics.UNKNOWN_FORMATTER(value), {
                    node,
                    word: value,
                });
            }
        }
    }, true);

    let outputValue = '';
    let topLevelType = null;
    let runtimeValue = null;
    let typeError: Error | undefined = undefined;
    for (const n of parsedValue.nodes) {
        if (n.type === 'function') {
            const matchingType = resolvedSymbols.customValues[n.value];

            if (matchingType) {
                try {
                    topLevelType = matchingType.evalVarAst(n, resolvedSymbols.customValues, true);
                    runtimeValue = unbox(topLevelType, true, resolvedSymbols.customValues, n);
                    try {
                        outputValue += matchingType.getValue(
                            args,
                            topLevelType,
                            n,
                            resolvedSymbols.customValues
                        );
                    } catch (error) {
                        if (error instanceof CustomValueError) {
                            outputValue += error.fallbackValue;
                        } else {
                            throw error;
                        }
                    }
                } catch (e) {
                    typeError = e as Error;

                    const invalidNode = initialNode || node;

                    if (invalidNode) {
                        diagnostics.report(
                            STVar.diagnostics.COULD_NOT_RESOLVE_VALUE(
                                [...(rootArgument ? [rootArgument] : []), ...args].join(', ')
                            ),
                            {
                                node: invalidNode,
                                word: value,
                            }
                        );
                    } else {
                        // TODO: catch broken variable resolutions without a node
                    }
                }
            } else {
                outputValue += getStringValue([n]);
            }
        } else {
            outputValue += getStringValue([n]);
        }
    }
    return { outputValue, topLevelType, typeError, runtimeValue };
}

export function evalDeclarationValue(
    resolver: StylableResolver,
    value: string,
    meta: StylableMeta,
    node?: postcss.Node,
    variableOverride?: Record<string, string> | null,
    valueHook?: replaceValueHook,
    diagnostics?: Diagnostics,
    passedThrough: string[] = [],
    args: string[] = [],
    getResolvedSymbols: (meta: StylableMeta) => MetaResolvedSymbols = createSymbolResolverWithCache(
        resolver,
        diagnostics || new Diagnostics()
    )
): string {
    return processDeclarationValue(
        resolver,
        getResolvedSymbols,
        value,
        meta,
        node,
        variableOverride,
        valueHook,
        diagnostics,
        passedThrough,
        args
    ).outputValue;
}
