// @ts-check
/** @import { EvalContext, CSSOutput, TreeVisitor, FileInfo } from './node.js' */
import Node from './node.js';
import Anonymous from './anonymous.js';
import FunctionCaller from '../functions/function-caller.js';

//
// A function call node.
//
class Call extends Node {
    get type() { return 'Call'; }

    /**
     * @param {string} name
     * @param {Node[]} args
     * @param {number} index
     * @param {FileInfo} currentFileInfo
     */
    constructor(name, args, index, currentFileInfo) {
        super();
        this.name = name;
        this.args = args;
        this.calc = name === 'calc';
        this._index = index;
        this._fileInfo = currentFileInfo;
    }

    /** @param {TreeVisitor} visitor */
    accept(visitor) {
        if (this.args) {
            this.args = visitor.visitArray(this.args);
        }
    }

    //
    // When evaluating a function call,
    // we either find the function in the functionRegistry,
    // in which case we call it, passing the  evaluated arguments,
    // if this returns null or we cannot find the function, we
    // simply print it out as it appeared originally [2].
    //
    // The reason why we evaluate the arguments, is in the case where
    // we try to pass a variable to a function, like: `saturate(@color)`.
    // The function should receive the value, not the variable.
    //
    /**
     * @param {EvalContext} context
     * @returns {Node}
     */
    eval(context) {
        /**
         * Turn off math for calc(), and switch back on for evaluating nested functions
         */
        const currentMathContext = context.mathOn;
        context.mathOn = !this.calc;
        if (this.calc || context.inCalc) {
            context.enterCalc();
        }

        const exitCalc = () => {
            if (this.calc || context.inCalc) {
                context.exitCalc();
            }
            context.mathOn = currentMathContext;
        };

        /** @type {Node | string | boolean | null | undefined} */
        let result;
        const funcCaller = new FunctionCaller(this.name, context, this.getIndex(), this.fileInfo());

        if (funcCaller.isValid()) {
            try {
                result = funcCaller.call(this.args);
                exitCalc();
            } catch (e) {
                // eslint-disable-next-line no-prototype-builtins
                if (/** @type {Record<string, unknown>} */ (e).hasOwnProperty('line') && /** @type {Record<string, unknown>} */ (e).hasOwnProperty('column')) {
                    throw e;
                }
                throw {
                    type: /** @type {Record<string, string>} */ (e).type || 'Runtime',
                    message: `Error evaluating function \`${this.name}\`${/** @type {Error} */ (e).message ? `: ${/** @type {Error} */ (e).message}` : ''}`,
                    index: this.getIndex(),
                    filename: this.fileInfo().filename,
                    line: /** @type {Record<string, number>} */ (e).lineNumber,
                    column: /** @type {Record<string, number>} */ (e).columnNumber
                };
            }
        }

        if (result !== null && result !== undefined) {
            // Results that that are not nodes are cast as Anonymous nodes
            // Falsy values or booleans are returned as empty nodes
            if (!(result instanceof Node)) {
                if (!result || result === true) {
                    result = new Anonymous(null);
                }
                else {
                    result = new Anonymous(result.toString());
                }

            }
            result._index = this._index;
            result._fileInfo = this._fileInfo;
            return result;
        }

        const args = this.args.map(a => a.eval(context));
        exitCalc();

        return new Call(this.name, args, this.getIndex(), this.fileInfo());
    }

    /**
     * @param {EvalContext} context
     * @param {CSSOutput} output
     */
    genCSS(context, output) {
        output.add(`${this.name}(`, this.fileInfo(), this.getIndex());

        for (let i = 0; i < this.args.length; i++) {
            this.args[i].genCSS(context, output);
            if (i + 1 < this.args.length) {
                output.add(', ');
            }
        }

        output.add(')');
    }
}

export default Call;
