"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QueryLanguageService = void 0;
const tslib_1 = require("tslib");
const rxjs_1 = require("rxjs");
const parser = tslib_1.__importStar(require("../../parser/src"));
const aggregatedBooleanExpressionFunctions_1 = require("../ExpressionFunctions/aggregatedBooleanExpressionFunctions");
const aggregatedScalarExpressionFunctions_1 = require("../ExpressionFunctions/aggregatedScalarExpressionFunctions");
const booleanExpressionFunctions_1 = require("../ExpressionFunctions/booleanExpressionFunctions");
const expressionFunctionUtils_1 = require("../ExpressionFunctions/expressionFunctionUtils");
const observableExpressionFunctions_1 = require("../ExpressionFunctions/observableExpressionFunctions");
const scalarExpressionFunctions_1 = require("../ExpressionFunctions/scalarExpressionFunctions");
const TypeExtensions_1 = require("../Extensions/TypeExtensions");
const AggregatedScalarLiveValue_1 = require("./AggregatedScalarLiveValue");
class QueryLanguageService {
    adaptableApi;
    cacheBooleanValidation = new Map();
    cacheObservableValidation = new Map();
    cacheAggregatedBooleanValidation = new Map();
    cacheAggregatedScalarValidation = new Map();
    cacheModuleSpecificExpressionFunctions = new Map();
    constructor(adaptableApi) {
        this.adaptableApi = adaptableApi;
    }
    evaluateBooleanExpression(expression, module, rowNode, evalContext) {
        if (expression == undefined) {
            // should never happen, but just in case
            this.adaptableApi.logError('QueryLanguageService.evaluateBooleanExpression was called with an undefined expression');
            return false;
        }
        const moduleExpressionFunctions = this.getModuleExpressionFunctionsMap(module);
        const booleanAndScalarFunctions = this.getBooleanAndScalarFunctions(moduleExpressionFunctions);
        return parser.evaluate(expression, {
            node: rowNode,
            functions: booleanAndScalarFunctions,
            evaluateCustomQueryVariable: this.evaluateCustomQueryVariable,
            dataChangedEvent: evalContext?.dataChangedEvent,
            pivotResultColumn: evalContext?.pivotResultColumn,
            ...this.adaptableApi.internalApi.buildBaseContext(),
        });
    }
    evaluateScalarExpression(expression, module, rowNode) {
        // currently scalar and boolean expressions are evaluated the same
        return this.evaluateBooleanExpression(expression, module, rowNode);
    }
    evaluateAggregatedScalarExpression(expression, module, getRowNodes) {
        const aggregatedScalarFunctions = this.getModuleExpressionFunctionsMap(module).aggregatedScalarFunctions;
        return parser.evaluate(expression, {
            node: null, // no node as this is an aggregation query
            functions: aggregatedScalarFunctions,
            evaluateCustomQueryVariable: this.evaluateCustomQueryVariable,
            getRowNodes,
            ...this.adaptableApi.internalApi.buildBaseContext(),
        });
    }
    evaluateObservableExpression(reactiveExpression, module) {
        const moduleExpressionFunctions = this.getModuleExpressionFunctionsMap(module);
        const booleanAndScalarFunctions = this.getBooleanAndScalarFunctions(moduleExpressionFunctions);
        const reactiveExpression$ = parser.evaluate(reactiveExpression, {
            node: this.adaptableApi.gridApi.getFirstRowNode(),
            functions: moduleExpressionFunctions.observableFunctions,
            whereClauseFunctions: booleanAndScalarFunctions,
            evaluateCustomQueryVariable: this.evaluateCustomQueryVariable,
            ...this.adaptableApi.internalApi.buildBaseContext(),
        });
        return reactiveExpression$;
    }
    evaluateAggregatedBooleanExpression(aggregationExpression, module) {
        const moduleExpressionFunctionsMap = this.getModuleExpressionFunctionsMap(module);
        const booleanAndScalarFunctions = this.getBooleanAndScalarFunctions(moduleExpressionFunctionsMap);
        const aggregationEvaluation = parser.evaluate(aggregationExpression, {
            node: this.adaptableApi.gridApi.getFirstRowNode(),
            functions: moduleExpressionFunctionsMap.aggregatedBooleanFunctions,
            whereClauseFunctions: booleanAndScalarFunctions,
            evaluateCustomQueryVariable: this.evaluateCustomQueryVariable,
            ...this.adaptableApi.internalApi.buildBaseContext(),
        });
        return aggregationEvaluation;
    }
    validateBoolean(expressionInput = '', module, config = { force: false }) {
        const cacheKey = this.getExpressionCacheKey(expressionInput, module);
        try {
            if (this.cacheBooleanValidation.has(cacheKey)) {
                return this.cacheBooleanValidation.get(cacheKey);
            }
            const expression = expressionInput.trim();
            if (expression === '') {
                const result = {
                    isValid: false,
                    errorMessage: 'empty boolean expression',
                };
                this.cacheBooleanValidation.set(cacheKey, result);
                return result;
            }
            const force = config?.force ?? false;
            // see if validation should be performed
            if (!this.adaptableApi.optionsApi.getExpressionOptions().performExpressionValidation &&
                !force) {
                const result = { isValid: true, errorMessage: '' };
                this.cacheBooleanValidation.set(cacheKey, result);
                return result;
            }
            const { ast } = parser.parse(expression.trim());
            const rootFn = ast[ast.length - 1];
            const moduleExpressionFunctions = this.getModuleExpressionFunctionsMap(module);
            const booleanAndScalarFunctions = this.getBooleanAndScalarFunctions(moduleExpressionFunctions);
            if (rootFn.type === undefined || booleanAndScalarFunctions[rootFn.type] === undefined) {
                const result = {
                    isValid: false,
                    errorMessage: `provided function ${rootFn?.type} cannot be handled`,
                };
                this.cacheBooleanValidation.set(cacheKey, result);
                return result;
            }
            if (booleanAndScalarFunctions[rootFn.type].returnType !== 'boolean') {
                const result = {
                    isValid: false,
                    errorMessage: `provided function ${rootFn?.type} is not a predicate`,
                };
                this.cacheBooleanValidation.set(cacheKey, result);
                return result;
            }
            // evaluating the expression is the only way to catch circular named query references
            const firstRowNode = this.adaptableApi.gridApi.getFirstRowNode();
            // Mock datachangedevent for change-based functions e.g. PERCENT_CHANGE
            const dataChangedEvent = {
                newValue: 100,
                oldValue: 150,
            };
            this.evaluateBooleanExpression(expression, module, firstRowNode, { dataChangedEvent });
            const result = {
                isValid: true,
                errorMessage: '',
            };
            this.cacheBooleanValidation.set(cacheKey, result);
            return result;
        }
        catch (error) {
            const result = { isValid: false, errorMessage: error };
            this.cacheBooleanValidation.set(cacheKey, result);
            return result;
        }
    }
    validateObservable(expressionInput = '', module) {
        const cacheKey = this.getExpressionCacheKey(expressionInput, module);
        if (this.cacheObservableValidation.has(cacheKey)) {
            return this.cacheObservableValidation.get(cacheKey);
        }
        const expression = expressionInput.trim();
        if (expression === '') {
            const result = {
                isValid: false,
                errorMessage: 'empty observable expression',
            };
            this.cacheObservableValidation.set(cacheKey, result);
            return result;
        }
        try {
            const observableExpression = this.evaluateObservableExpression(expression, module);
            if (!(0, rxjs_1.isObservable)(observableExpression)) {
                const result = {
                    isValid: false,
                    errorMessage: `provided observable expression does not evaluate to an Observable`,
                };
                this.cacheObservableValidation.set(cacheKey, result);
                return result;
            }
            const result = { isValid: true, errorMessage: '' };
            this.cacheObservableValidation.set(cacheKey, result);
            return result;
        }
        catch (error) {
            const result = {
                isValid: false,
                errorMessage: error,
            };
            this.cacheObservableValidation.set(cacheKey, result);
            return result;
        }
    }
    validateAggregatedBoolean(expressionInput = '', module) {
        const cacheKey = this.getExpressionCacheKey(expressionInput, module);
        if (this.cacheAggregatedBooleanValidation.has(cacheKey)) {
            return this.cacheAggregatedBooleanValidation.get(cacheKey);
        }
        const expression = expressionInput.trim();
        if (expression === '') {
            const result = {
                isValid: false,
                errorMessage: 'empty AggregatedBoolean expression',
            };
            this.cacheAggregatedBooleanValidation.set(cacheKey, result);
            return result;
        }
        try {
            const evaluationResult = this.evaluateAggregatedBooleanExpression(expression, module);
            if (evaluationResult.type !== 'aggregationBoolean') {
                const result = {
                    isValid: false,
                    errorMessage: 'provided AggregatedBoolean expression does not evaluate to a supported aggregation',
                };
                this.cacheAggregatedBooleanValidation.set(cacheKey, result);
                return result;
            }
            // no exception,so everything seems to be fine
            const result = { isValid: true, errorMessage: '' };
            this.cacheAggregatedBooleanValidation.set(cacheKey, result);
            return result;
        }
        catch (error) {
            const result = {
                isValid: false,
                errorMessage: error,
            };
            this.cacheAggregatedBooleanValidation.set(cacheKey, result);
            return result;
        }
    }
    validateAggregatedScalar(expressionInput = '', module) {
        const cacheKey = this.getExpressionCacheKey(expressionInput, module);
        if (this.cacheAggregatedScalarValidation.has(cacheKey)) {
            return this.cacheAggregatedScalarValidation.get(cacheKey);
        }
        const expression = expressionInput.trim();
        if (expression === '') {
            const result = {
                isValid: false,
                errorMessage: 'empty AggregatedScalar expression',
            };
            this.cacheAggregatedScalarValidation.set(cacheKey, result);
            return result;
        }
        try {
            const evaluationResult = this.evaluateAggregatedScalarExpression(expression, module);
            if (evaluationResult.type !== 'aggregationScalar') {
                const result = {
                    isValid: false,
                    errorMessage: 'provided AggregatedScalar expression does not evaluate to a supported aggregation',
                };
                this.cacheAggregatedScalarValidation.set(cacheKey, result);
                return result;
            }
            // no exception,so everything seems to be fine
            const result = { isValid: true, errorMessage: '' };
            this.cacheAggregatedScalarValidation.set(cacheKey, result);
            return result;
        }
        catch (error) {
            const result = {
                isValid: false,
                errorMessage: error,
            };
            this.cacheAggregatedScalarValidation.set(cacheKey, result);
            return result;
        }
    }
    computeAggregatedBooleanValue(expression, module) {
        const booleanAggregationParameter = this.evaluateAggregatedBooleanExpression(expression, module);
        const aggregatedScalarExpressionEvaluation = booleanAggregationParameter.scalarAggregation.value;
        const aggregatedScalarLiveValue = new AggregatedScalarLiveValue_1.AggregatedScalarLiveValue({ aggregatedScalarExpressionEvaluation }, module, this.adaptableApi);
        const allAggregationValues = aggregatedScalarLiveValue.getAllAggregationValues();
        const numericOperand = booleanAggregationParameter.conditionValue;
        const booleanConditionFn = booleanAggregationParameter.conditionFn;
        return allAggregationValues.some((aggregationValue) => {
            return booleanConditionFn(aggregationValue, numericOperand);
        });
    }
    getColumnsFromExpression(input = '') {
        return this.getNodesFromExpression(input, 'COL');
    }
    getNamedQueryNamesFromExpression(input = '') {
        return this.getNodesFromExpression(input, 'QUERY');
    }
    getExpressionWithColumnFriendlyNames(expression = '') {
        let result = expression;
        const columnIds = this.getColumnsFromExpression(expression);
        columnIds.forEach((columnId) => {
            const columnFriendlyName = this.adaptableApi.columnApi.getFriendlyNameForColumnId(columnId);
            result = result.split(columnId).join(columnFriendlyName);
        });
        return result;
    }
    // Returns the ExpressionFunctions available for the given Module as specified in the `QueryLanguageOptions.moduleExpressionFunctions`
    // if there are no specific functions defined, it falls back to the default values
    getModuleExpressionFunctionsMap(module) {
        const expressionOptions = this.adaptableApi.optionsApi.getExpressionOptions();
        if (module) {
            let cachedResult = this.cacheModuleSpecificExpressionFunctions.get(module);
            if (cachedResult) {
                return cachedResult;
            }
        }
        const generalBooleanExpressionFunctions = this.extractMappedExpressionFunctions(booleanExpressionFunctions_1.booleanExpressionFunctions, expressionOptions.systemBooleanFunctions, expressionOptions.customBooleanFunctions);
        const generalScalarExpressionFunctions = this.extractMappedExpressionFunctions(scalarExpressionFunctions_1.scalarExpressionFunctions, expressionOptions.systemScalarFunctions, expressionOptions.customScalarFunctions);
        const generalObservableExpressionFunctions = this.extractMappedExpressionFunctions(observableExpressionFunctions_1.observableExpressionFunctions, expressionOptions.systemObservableFunctions);
        const generalAggregatedBooleanExpressionFunctions = this.extractMappedExpressionFunctions(aggregatedBooleanExpressionFunctions_1.aggregatedBooleanExpressionFunctions, expressionOptions.systemAggregatedBooleanFunctions, 
        // right now the custom aggregated scalar functions are used in the aggregated boolean functions as well
        // hopefully we'll simplify this in the future
        this.getCustomAggregatedScalarFunctions());
        const generalAggregatedScalarExpressionFunctions = this.extractMappedExpressionFunctions(aggregatedScalarExpressionFunctions_1.aggregatedScalarExpressionFunctions, expressionOptions.systemAggregatedScalarFunctions, this.getCustomAggregatedScalarFunctions());
        if (!module) {
            this.adaptableApi.logWarn(`QueryLanguageService.getModuleExpressionFunctions() was called with an undefined 'module' param, this should never happen`);
            return {
                booleanFunctions: generalBooleanExpressionFunctions,
                scalarFunctions: generalScalarExpressionFunctions,
                observableFunctions: generalObservableExpressionFunctions,
                aggregatedBooleanFunctions: generalAggregatedBooleanExpressionFunctions,
                aggregatedScalarFunctions: generalAggregatedScalarExpressionFunctions,
            };
        }
        let moduleExpressionFunctions;
        if (typeof expressionOptions.moduleExpressionFunctions === 'function') {
            const context = {
                module,
                availableBooleanFunctionNames: (0, TypeExtensions_1.getTypedKeys)(generalBooleanExpressionFunctions),
                availableScalarFunctionNames: (0, TypeExtensions_1.getTypedKeys)(generalScalarExpressionFunctions),
                availableObservableFunctionNames: (0, TypeExtensions_1.getTypedKeys)(generalObservableExpressionFunctions),
                availableAggregatedBooleanFunctionNames: (0, TypeExtensions_1.getTypedKeys)(generalAggregatedBooleanExpressionFunctions),
                availableAggregatedScalarFunctionNames: (0, TypeExtensions_1.getTypedKeys)(generalAggregatedScalarExpressionFunctions),
                ...this.adaptableApi.internalApi.buildBaseContext(),
            };
            moduleExpressionFunctions = expressionOptions.moduleExpressionFunctions(context) ?? {};
        }
        else {
            moduleExpressionFunctions = expressionOptions.moduleExpressionFunctions?.[module] ?? {};
        }
        const moduleExpressionFunctionsMap = {
            booleanFunctions: this.extractMappedExpressionFunctions(generalBooleanExpressionFunctions, moduleExpressionFunctions.systemBooleanFunctions, moduleExpressionFunctions.customBooleanFunctions),
            scalarFunctions: this.extractMappedExpressionFunctions(generalScalarExpressionFunctions, moduleExpressionFunctions.systemScalarFunctions, moduleExpressionFunctions.customScalarFunctions),
            observableFunctions: this.extractMappedExpressionFunctions(generalObservableExpressionFunctions, moduleExpressionFunctions.systemObservableFunctions),
            aggregatedBooleanFunctions: this.extractMappedExpressionFunctions(generalAggregatedBooleanExpressionFunctions, moduleExpressionFunctions.systemAggregatedBooleanFunctions),
            aggregatedScalarFunctions: this.extractMappedExpressionFunctions(generalAggregatedScalarExpressionFunctions, moduleExpressionFunctions.systemAggregatedScalarFunctions),
        };
        this.cacheModuleSpecificExpressionFunctions.set(module, moduleExpressionFunctionsMap);
        return moduleExpressionFunctionsMap;
    }
    getCustomAggregatedScalarFunctions() {
        const customAggregatedScalarFunctions = this.adaptableApi.optionsApi.getExpressionOptions()?.customAggregatedFunctions;
        if (!customAggregatedScalarFunctions) {
            return {};
        }
        if (typeof customAggregatedScalarFunctions === 'function') {
            return (context) => {
                const customDefinitions = customAggregatedScalarFunctions(context);
                return this.mapCustomAggregatedScalarFunctionsToInternal(customDefinitions);
            };
        }
        return this.mapCustomAggregatedScalarFunctionsToInternal(customAggregatedScalarFunctions);
    }
    mapCustomAggregatedScalarFunctionsToInternal(aggregatedScalarFunctions) {
        const getValueForColId = (colId, rowNode) => {
            return this.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, colId);
        };
        return Object.entries(aggregatedScalarFunctions).reduce((acc, [expressionName, customExpressionDefinition]) => {
            const expression = {
                handler: (args, context) => {
                    const [aggColParam, ...restOfColParams] = (0, expressionFunctionUtils_1.extractColumnParameters)(expressionName, args);
                    const aggColumnId = aggColParam?.value;
                    const groupByParameter = (0, expressionFunctionUtils_1.extractParameter)(expressionName, 'operand', ['GROUP_BY'], args, {
                        isOptional: true,
                    });
                    const groupByColumnIds = groupByParameter?.value;
                    const contextArgs = args.filter((arg) => {
                        if (typeof arg === 'object' && arg.name === 'COL') {
                            // filter out aggregation column
                            return arg.value !== aggColumnId;
                        }
                        if (typeof arg === 'object' && arg.name === 'GROUP_BY') {
                            // filter out groupBy column
                            return false;
                        }
                        return true;
                    });
                    const aggregationExpressionEvaluation = {
                        aggregationParams: {
                            reducers: {
                                [expressionName]: {
                                    name: expressionName,
                                    field: aggColumnId,
                                    initialValue: customExpressionDefinition.initialValue,
                                    reducer: (acc, value, rowNode, dataIndex) => {
                                        const context = {
                                            accumulator: acc,
                                            currentValue: value,
                                            index: dataIndex,
                                            rowNode,
                                            args: contextArgs.map((arg) => {
                                                // if col, replace with value
                                                if (typeof arg === 'object' && arg.name === 'COL') {
                                                    return getValueForColId(arg.value, rowNode);
                                                }
                                                return arg;
                                            }),
                                            aggColumnId,
                                            groupByColumnIds,
                                            getValueForColId: (colId) => getValueForColId(colId, rowNode),
                                            ...this.adaptableApi.internalApi.buildBaseContext(),
                                        };
                                        return customExpressionDefinition.reducer(context);
                                    },
                                    done: (accValue, dataArray) => {
                                        if (customExpressionDefinition.processAggregatedValue) {
                                            const context = {
                                                ...this.adaptableApi.internalApi.buildBaseContext(),
                                                aggregatedValue: accValue,
                                                rowNodes: dataArray,
                                                args: contextArgs,
                                                aggColumnId,
                                                groupByColumnIds,
                                            };
                                            return customExpressionDefinition.processAggregatedValue(context);
                                        }
                                        return accValue;
                                    },
                                },
                            },
                        },
                        rowValueGetter: (rowNode, aggregationValue) => {
                            if (customExpressionDefinition.prepareRowValue) {
                                const context = {
                                    rowNode: rowNode,
                                    aggregatedValue: aggregationValue,
                                    ...this.adaptableApi.internalApi.buildBaseContext(),
                                    args: contextArgs.map((arg) => {
                                        // if col, replace with value
                                        if (typeof arg === 'object' && arg.name === 'COL') {
                                            return getValueForColId(arg.value, rowNode);
                                        }
                                        return arg;
                                    }),
                                    aggColumnId,
                                    groupByColumnIds,
                                    getValueForColId: (colId) => getValueForColId(colId, rowNode),
                                };
                                return customExpressionDefinition.prepareRowValue(context);
                            }
                            return aggregationValue;
                        },
                        rowFilterFn: (rowNode) => {
                            if (customExpressionDefinition.filterRow) {
                                return customExpressionDefinition.filterRow({
                                    rowNode,
                                    ...this.adaptableApi.internalApi.buildBaseContext(),
                                    args: contextArgs.map((arg) => {
                                        // if col, replace with value
                                        if (typeof arg === 'object' && arg.name === 'COL') {
                                            return getValueForColId(arg.value, rowNode);
                                        }
                                        return arg;
                                    }),
                                    aggColumnId,
                                    groupByColumnIds,
                                    getValueForColId: (colId) => getValueForColId(colId, rowNode),
                                });
                            }
                            return context?.filterFn?.(rowNode) ?? true;
                        },
                        getRowNodes: context.getRowNodes,
                    };
                    (0, aggregatedScalarExpressionFunctions_1.addGroupByParams)(groupByParameter?.value, aggregationExpressionEvaluation);
                    const result = {
                        name: expressionName,
                        type: 'aggregationScalar',
                        value: aggregationExpressionEvaluation,
                    };
                    return result;
                },
                description: customExpressionDefinition.description,
                signatures: customExpressionDefinition.signatures,
                inputs: customExpressionDefinition.inputTypes,
            };
            acc[expressionName] = expression;
            return acc;
        }, {});
    }
    extractMappedExpressionFunctions(availableExpressionFunctions, systemFunctions, customFunctions) {
        const systemFunctionNames = typeof systemFunctions === 'function'
            ? systemFunctions({
                availableExpressionFunctionNames: (0, TypeExtensions_1.getTypedKeys)(availableExpressionFunctions),
                ...this.adaptableApi.internalApi.buildBaseContext(),
            })
            : systemFunctions;
        let generalExpressionFunctions = {};
        // add system functions
        if (Array.isArray(systemFunctionNames)) {
            // add only system functions specified by user
            systemFunctionNames.forEach((systemFunctionName) => {
                generalExpressionFunctions[systemFunctionName] =
                    availableExpressionFunctions[systemFunctionName];
            });
        }
        else {
            // add ALL system functions
            generalExpressionFunctions = { ...availableExpressionFunctions };
        }
        const customFunctionDefinitions = typeof customFunctions === 'function'
            ? customFunctions({
                availableExpressionFunctionNames: (0, TypeExtensions_1.getTypedKeys)(generalExpressionFunctions),
                ...this.adaptableApi.internalApi.buildBaseContext(),
            })
            : customFunctions;
        if (customFunctionDefinitions) {
            generalExpressionFunctions = { ...generalExpressionFunctions, ...customFunctionDefinitions };
        }
        return generalExpressionFunctions;
    }
    evaluateCustomQueryVariable(functionName, args) {
        const context = {
            args,
            ...this.adaptableApi.internalApi.buildBaseContext(),
        };
        const customQueryVariableDefinition = this.adaptableApi.optionsApi.getExpressionOptions()?.customQueryVariables?.[functionName];
        return typeof customQueryVariableDefinition === 'function'
            ? customQueryVariableDefinition(context)
            : customQueryVariableDefinition;
    }
    getBooleanAndScalarFunctions(moduleExpressionFunctionsMap) {
        return {
            ...moduleExpressionFunctionsMap.booleanFunctions,
            ...moduleExpressionFunctionsMap.scalarFunctions,
        };
    }
    getExpressionCacheKey(expression, module) {
        return `${module}::${expression}`;
    }
    getNodesFromExpression(input, nodeType) {
        try {
            const resultSet = new Set();
            // @ts-ignore
            const walk = (node) => {
                if (typeof node !== 'object') {
                    return false;
                }
                if (Array.isArray(node)) {
                    return node.map(walk);
                }
                node.args.map(walk);
                if (node.type === nodeType) {
                    resultSet.add(String(node.args[0]));
                }
            };
            const { ast } = parser.parse(input.trim());
            walk(ast);
            return Array.from(resultSet.values());
        }
        catch (e) {
            return [];
        }
    }
}
exports.QueryLanguageService = QueryLanguageService;
