"use strict";
/*
 * @adonisjs/validator
 *
 * (c) Harminder Virk <virk@adonisjs.com>
 *
 * For the full copyright and license information, please view the LICENSE
 * file that was distributed with this source code.
 */
Object.defineProperty(exports, "__esModule", { value: true });
exports.Compiler = void 0;
const Buffer_1 = require("./Buffer");
const Array_1 = require("./Nodes/Array");
const Object_1 = require("./Nodes/Object");
const Literal_1 = require("./Nodes/Literal");
/**
 * Compiler exposes the API to compile the schema tree into a set of Javascript
 * instructions.
 */
class Compiler {
    constructor(schema) {
        this.schema = schema;
        /**
         * Reference counters. They are required for node compilers to
         * create safe and unique variable names
         */
        this.outVariableCounter = 0;
        this.referenceVariableCounter = 0;
        this.arrayIndexVariableCounter = 0;
        /**
         * The name of certain properties referenced inside
         * the compiled output
         */
        this.COMPILER_REFERENCES = {
            validations: 'validations',
            exists: 'helpers.exists',
            isObject: 'helpers.isObject',
            reportError: 'errorReporter',
        };
    }
    /**
     * Returns the name of the options object for a given variable
     */
    getVariableOptionsName(variableName) {
        return `${variableName}_options`;
    }
    /**
     * Returns the name of the mutation function for a given variable
     */
    getVariableMutationName(variableName) {
        return `mutate_${variableName}`;
    }
    /**
     * The variable name to hold the boolean if value is undefined or not
     */
    getVariableExistsName(variableName) {
        return `${variableName}_exists`;
    }
    /**
     * Returns the declaration for the undefined and the null check
     */
    getVariableExistsDeclaration(variableName) {
        return `let ${this.getVariableExistsName(variableName)} = ${this.COMPILER_REFERENCES.exists}(${variableName})`;
    }
    /**
     * Returns the expression to declare the mutation function for a given
     * field.
     */
    getMutationFnDeclararationExpression(variableName) {
        return `function ${this.getVariableMutationName(variableName)} (newValue) {
      ${variableName} = newValue;
      ${this.getVariableExistsName(variableName)} = ${this.COMPILER_REFERENCES.exists}(${variableName});
    }`;
    }
    /**
     * Returns the expression to declare the options
     */
    getOptionsDeclarationExpression(variableName, field, tip, pointer, arrayExpressionPointer) {
        const arrayExpressionPointerItem = arrayExpressionPointer
            ? `\n  arrayExpressionPointer: ${arrayExpressionPointer},`
            : '';
        return `const ${this.getVariableOptionsName(variableName)} = {
      root,
      refs,
      field: ${field},
      tip: ${tip},
      pointer: ${pointer},${arrayExpressionPointerItem}
      mutate: ${this.getVariableMutationName(variableName)},
      ${this.COMPILER_REFERENCES.reportError}
    }`;
    }
    /**
     * Returns the method call expression for executing validation
     * a given rule
     */
    getValidationCallableExpression(variableName, rule) {
        const compiledOptions = JSON.stringify(rule.compiledOptions);
        const options = this.getVariableOptionsName(variableName);
        /**
         * Use `validateAsync` when rule is async, otherwise use `validate`
         */
        const methodCall = rule.async
            ? `await ${this.COMPILER_REFERENCES.validations}.${rule.name}.validate`
            : `${this.COMPILER_REFERENCES.validations}.${rule.name}.validate`;
        /**
         * If rule doesn't want to get executed on undefined and null values, then make
         * sure to add the `exists` guard first
         */
        const existsGuard = rule.allowUndefineds
            ? ''
            : `${this.getVariableExistsName(variableName)} && `;
        const callableExpression = `${methodCall}(${variableName}, ${compiledOptions}, ${options})`;
        return `${existsGuard}${callableExpression}`;
    }
    /**
     * Converts a pointer to a Javascript expression
     */
    pointerToExpression(pointer) {
        return pointer.type === 'literal' ? `'${pointer.name}'` : pointer.name;
    }
    /**
     * Converts an array of pointers to a Javascript expression
     */
    pointersToExpression(pointer) {
        let hasIdentifiers = false;
        const pointerExpression = pointer
            .map((one) => {
            if (one.type === 'identifier') {
                hasIdentifiers = true;
                return `\$\{${one.name}}`;
            }
            return one.name;
        })
            .join('.');
        return hasIdentifiers ? `\`${pointerExpression}\`` : `'${pointerExpression}'`;
    }
    /**
     * Compiles a given node inside the schema tree. The compiled
     * output is written to the buffer
     */
    compileNode(field, node, buffer, parentPointer, referenceVariable, outVariable) {
        field = typeof field === 'string' ? { name: field, type: 'literal' } : field;
        /**
         * Reference object for node compilers
         */
        const references = {
            outVariable: outVariable || 'out',
            referenceVariable: referenceVariable || 'root',
            parentPointer,
        };
        switch (node.type) {
            case 'literal':
                new Literal_1.LiteralCompiler(field, node, this, references).compile(buffer);
                break;
            case 'object':
                new Object_1.ObjectCompiler(field, node, this, references).compile(buffer);
                break;
            case 'array':
                new Array_1.ArrayCompiler(field, node, this, references).compile(buffer);
                break;
        }
    }
    /**
     * Compiles all nodes for the schema tree recursively. The compiled
     * output is written to the buffer.
     */
    compileTree(tree, buffer, parentPointer, referenceVariable, outVariable) {
        Object.keys(tree).forEach((field) => {
            this.compileNode(field, tree[field], buffer, parentPointer, referenceVariable, outVariable);
        });
    }
    /**
     * Compiles the tree and returns the compiled code as a string.
     */
    compileAsString() {
        const buffer = new Buffer_1.CompilerBuffer();
        /**
         * We wrap the buffer output inside an async function with following arguments.
         *
         * - `root` is the object to validate
         * - `validate` is the function to validate using synchronous rules
         * - `validateAsync` is the function to validate using asynchronous rules
         * - `isObject` checks whether the value is a valid Javascript object
         * - `exists` checks whether the value is defined or not.
         */
        buffer.wrappingCode([
            'return async function (root, validations, errorReporter, helpers, refs) {',
            '}',
        ]);
        /**
         * Declaring the out variable. The compiler code will write the
         * validated properties to this variable
         */
        buffer.writeExpression('const out = {}');
        /**
         * Parse the schema
         */
        this.compileTree(this.schema, buffer, []);
        /**
         * Return the out value
         */
        buffer.writeExpression(`
      if (errorReporter.hasErrors) {
        throw errorReporter.toError();
      }

      return out`, true);
        buffer.dedent();
        /**
         * Return the buffer string
         */
        return buffer.flush();
    }
    /**
     * Compiles the schema tree to an executable function
     */
    compile() {
        return new Function(this.compileAsString())();
    }
}
exports.Compiler = Compiler;
