"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.addGroupByParams = exports.aggregatedScalarExpressionFunctionNames = exports.aggregatedScalarExpressionFunctions = exports.quantileAggregatedExpressionFunctions = exports.cumulativeAggregatedExpressionFunctions = exports.aggregatedExpressionFunctions = void 0;
const tslib_1 = require("tslib");
const ExpressionEvaluationError_1 = require("../../parser/src/ExpressionEvaluationError");
const ArrayExtensions_1 = require("../Extensions/ArrayExtensions");
const TypeExtensions_1 = require("../Extensions/TypeExtensions");
const expressionFunctionUtils_1 = require("./expressionFunctionUtils");
const isAfter_1 = tslib_1.__importDefault(require("date-fns/isAfter"));
const DateHelper_1 = require("../Helpers/DateHelper");
exports.aggregatedExpressionFunctions = [
    'SUM',
    'PERCENTAGE',
    'MIN',
    'MAX',
    'AVG',
    'COUNT',
    'WEIGHT',
    'COL',
    'GROUP_BY',
    'MEDIAN',
    'MODE',
    'DISTINCT',
    'ONLY',
    'STD_DEVIATION',
];
exports.cumulativeAggregatedExpressionFunctions = [
    'CUMUL',
    'OVER',
    'SUM',
    'PERCENTAGE',
    'MIN',
    'MAX',
    'AVG',
    'WEIGHT',
    'COL',
    'GROUP_BY',
];
exports.quantileAggregatedExpressionFunctions = [
    'QUANT',
    'QUARTILE',
    'PERCENTILE',
    'COL',
    'GROUP_BY',
];
exports.aggregatedScalarExpressionFunctions = {
    CUMUL: {
        handler(args, context) {
            const aggregationParameter = (0, expressionFunctionUtils_1.extractParameter)('CUMUL', 'aggregationScalar', ['SUM', 'PERCENTAGE', 'AVG', 'MIN', 'MAX', 'COUNT'], args);
            const overColumnParameter = (0, expressionFunctionUtils_1.extractParameter)('CUMUL', 'operand', ['OVER'], args);
            const groupByParameter = (0, expressionFunctionUtils_1.extractParameter)(aggregationParameter.name, 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            if (!!groupByParameter) {
                // ensure the GROUP_BY is NOT on the CUMUL function
                // it should be on the AGGREGATION function
                throw new ExpressionEvaluationError_1.ExpressionEvaluationError('GROUP_BY', `should be passed as an argument to the '${aggregationParameter.name}' aggregation function!`);
            }
            const cumulationExpressionEvaluation = mapAggregationToCumulation(aggregationParameter, overColumnParameter, context);
            const result = {
                name: 'CUMUL',
                type: 'aggregationScalar',
                value: cumulationExpressionEvaluation,
            };
            return result;
        },
        description: 'Performs a cumulative aggregation (running total) with the given aggregation operation over a provided column',
        signatures: ['CUMUL( aggregationOp: SUM|MIN|MAX, OVER( [colNameB] ))'],
        examples: [
            'CUMUL( SUM([colNameA]), OVER( [colNameB] ))',
            'CUMUL( MIN([colNameA]), OVER( [colNameB] ))',
            'CUMUL( MAX([colNameA]), OVER( [colNameB] ))',
        ],
        category: 'cumulative',
    },
    SUM: {
        handler(args, context) {
            const sumColumnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('SUM', args);
            const sumColumnName = sumColumnParameter.value;
            const columnType = (0, expressionFunctionUtils_1.validateColumnType)(sumColumnName, ['Number'], 'SUM', context.adaptableApi);
            const groupByParameter = (0, expressionFunctionUtils_1.extractParameter)('SUM', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        SUM: {
                            name: 'SUM',
                            field: sumColumnName,
                            initialValue: 0,
                            reducer: (totalSum, rowValue) => {
                                // TODO: see why error are not concole logged
                                // debuger = 'Adaptable:*'
                                if (isUndefinedValue(rowValue)) {
                                    return totalSum;
                                }
                                return totalSum + (0, expressionFunctionUtils_1.getNumericValue)(rowValue);
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            (0, exports.addGroupByParams)(groupByParameter === null || groupByParameter === void 0 ? void 0 : groupByParameter.value, aggregationExpressionEvaluation);
            const result = {
                name: 'SUM',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by summing up the column values\nOptionally the aggregation may be computed within provided individual groups',
        signatures: [
            'SUM( [colName] )',
            'SUM( COL(name: string))',
            'SUM( [colNameA], GROUP_BY( [colNameB] ))',
            'SUM( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['SUM([colA])', 'SUM([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: ['number'],
    },
    PERCENTAGE: {
        handler(args, context) {
            var _a;
            const percentageColumnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('PERCENTAGE', args);
            const percentageColumnName = percentageColumnParameter.value;
            const columnType = (0, expressionFunctionUtils_1.validateColumnType)(percentageColumnName, ['Number'], 'PERCENTAGE', context.adaptableApi);
            const sumOperand = (0, expressionFunctionUtils_1.extractParameter)('PERCENTAGE', 'aggregationScalar', ['SUM'], args, {
                isOptional: true,
            });
            const groupByOperand = (0, expressionFunctionUtils_1.extractParameter)('PERCENTAGE', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            if (sumOperand && groupByOperand) {
                throw new ExpressionEvaluationError_1.ExpressionEvaluationError('PERCENTAGE', `expects either a SUM or a GROUP_BY argument`);
            }
            const sumAggregationColumnName = sumOperand
                ? sumOperand.value.aggregationParams.reducers['SUM'].field
                : percentageColumnName;
            const groupByColumnNames = groupByOperand
                ? groupByOperand === null || groupByOperand === void 0 ? void 0 : groupByOperand.value
                : (_a = sumOperand === null || sumOperand === void 0 ? void 0 : sumOperand.value.aggregationParams.groupBy) === null || _a === void 0 ? void 0 : _a.map((param) => param.field);
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        PERCENTAGE: {
                            name: 'PERCENTAGE',
                            field: sumAggregationColumnName,
                            initialValue: 0,
                            reducer: (totalSum, rowValue) => {
                                if (isUndefinedValue(rowValue)) {
                                    return totalSum;
                                }
                                return totalSum + (0, expressionFunctionUtils_1.getNumericValue)(rowValue);
                            },
                        },
                    },
                },
                rowValueGetter: (rowNode, aggregationValue) => {
                    return (((0, expressionFunctionUtils_1.getNumericValue)(context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, percentageColumnName)) /
                        aggregationValue) *
                        100);
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            (0, exports.addGroupByParams)(groupByColumnNames, aggregationExpressionEvaluation);
            const result = {
                name: 'PERCENTAGE',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Divides each column value by the aggregated sum of a column\nThe column with the aggregated value and the column with the percentage value do not necessarily have to be identical',
        signatures: [
            'PERCENTAGE( [colName] )',
            'PERCENTAGE( [colNameA], GROUP_BY( [colNameB] ))',
            'PERCENTAGE( [colNameA], SUM ( [colNameB] ))',
            'PERCENTAGE( [colNameA], SUM ( [colNameB], GROUP_BY( [colNameC] )))',
        ],
        examples: [
            'PERCENTAGE( [colName] )',
            'PERCENTAGE( [colNameA], GROUP_BY( [colNameB] ))',
            'PERCENTAGE( [colNameA], SUM ( [colNameB] ))',
            'PERCENTAGE( [colNameA], SUM ( [colNameB], GROUP_BY( [colNameC] )))',
        ],
        category: 'aggregation',
    },
    AVG: {
        handler(args, context) {
            const avgColumnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('AVG', args);
            const avgColumnName = avgColumnParameter.value;
            const columnType = (0, expressionFunctionUtils_1.validateColumnType)(avgColumnName, ['Number'], 'AVG', context.adaptableApi);
            const groupByParameter = (0, expressionFunctionUtils_1.extractParameter)('AVG', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const weightParameter = (0, expressionFunctionUtils_1.extractParameter)('AVG', 'operand', ['WEIGHT'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        AVG: {
                            name: 'AVG',
                            field: avgColumnName,
                            initialValue: {
                                sumOfValues: 0,
                                numberOfValues: 0,
                            },
                            reducer: (aggregatedValue, rowValue, rowNode) => {
                                var _a;
                                if (isUndefinedValue(rowValue)) {
                                    return aggregatedValue;
                                }
                                const numericRowValue = (0, expressionFunctionUtils_1.getNumericValue)(rowValue);
                                if (weightParameter) {
                                    // weighted average
                                    const weightValue = (_a = (0, expressionFunctionUtils_1.getNumericValue)(context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, weightParameter.value))) !== null && _a !== void 0 ? _a : 0;
                                    aggregatedValue.sumOfValues =
                                        aggregatedValue.sumOfValues + numericRowValue * weightValue;
                                    aggregatedValue.numberOfValues = aggregatedValue.numberOfValues + weightValue;
                                }
                                else {
                                    aggregatedValue.sumOfValues = aggregatedValue.sumOfValues + numericRowValue;
                                    aggregatedValue.numberOfValues = aggregatedValue.numberOfValues + 1;
                                }
                                return aggregatedValue;
                            },
                            done: (aggregatedValue) => {
                                if (aggregatedValue.numberOfValues === 0) {
                                    return 0;
                                }
                                return aggregatedValue.sumOfValues / aggregatedValue.numberOfValues;
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            (0, exports.addGroupByParams)(groupByParameter === null || groupByParameter === void 0 ? void 0 : groupByParameter.value, aggregationExpressionEvaluation);
            if (weightParameter) {
                aggregationExpressionEvaluation.context = {
                    weightParam: weightParameter,
                };
            }
            const result = {
                name: 'AVG',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the average value (arithmetic mean) of the column values\nOptionally the aggregation may be weighted and/or grouped.',
        signatures: [
            'AVG( [colName] )',
            'AVG( COL(name: string))',
            'AVG( [colNameA], GROUP_BY( [colNameB] ))',
            'AVG( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['AVG([colA])', 'AVG([colA], GROUP_BY([colB]))', 'AVG([colA], WEIGHT([colB]))'],
        category: 'aggregation',
        inputs: ['number'],
    },
    MEDIAN: {
        handler(args, context) {
            const medianColumnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('MEDIAN', args);
            const medianColumnName = medianColumnParameter.value;
            const columnType = (0, expressionFunctionUtils_1.validateColumnType)(medianColumnName, ['Number'], 'MEDIAN', context.adaptableApi);
            const groupByParameter = (0, expressionFunctionUtils_1.extractParameter)('MEDIAN', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const getCellValue = (rowNode) => {
                return context.adaptableApi.gridApi.getNormalisedValueFromRowNode(rowNode, medianColumnName);
            };
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        MEDIAN: {
                            name: 'MEDIAN',
                            field: medianColumnName,
                            initialValue: [],
                            reducer: (aggregatedValue, value, rowNode) => {
                                aggregatedValue.push(rowNode);
                                return aggregatedValue;
                            },
                            done: (aggregatedValue) => {
                                if (aggregatedValue.length === 0) {
                                    return null;
                                }
                                if (aggregatedValue.length === 1) {
                                    return getCellValue(aggregatedValue[0]);
                                }
                                const lenght = aggregatedValue.length;
                                const middle = Math.floor((lenght - 1) / 2);
                                if (lenght % 2) {
                                    return getCellValue(aggregatedValue[middle]);
                                }
                                else {
                                    return ((getCellValue(aggregatedValue[middle]) +
                                        getCellValue(aggregatedValue[middle + 1])) /
                                        2.0);
                                }
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            (0, exports.addGroupByParams)(groupByParameter === null || groupByParameter === void 0 ? void 0 : groupByParameter.value, aggregationExpressionEvaluation);
            const result = {
                name: 'MEDIAN',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the median of the column values\nOptionally the aggregation may be grouped.',
        signatures: [
            'MEDIAN( [colName] )',
            'MEDIAN( COL(name: string))',
            'MEDIAN( [colNameA], GROUP_BY( [colNameB] ))',
            'MEDIAN( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['MEDIAN([colA])', 'MEDIAN([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: ['number'],
    },
    MODE: {
        handler(args, context) {
            const modeColumnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('mode', args);
            const modeColumnName = modeColumnParameter.value;
            const columnType = (0, expressionFunctionUtils_1.validateColumnType)(modeColumnName, ['Number', 'String', 'Date'], 'mode', context.adaptableApi);
            const groupByParameter = (0, expressionFunctionUtils_1.extractParameter)('MODE', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        MODE: {
                            name: 'MODE',
                            field: modeColumnName,
                            initialValue: new Map(),
                            reducer: (aggregatedValue, rowValue, rowNode) => {
                                var _a;
                                if (typeof rowValue !== 'number' && typeof rowValue !== 'string') {
                                    return aggregatedValue;
                                }
                                if (!aggregatedValue) {
                                    return new Map();
                                }
                                aggregatedValue.set(rowValue, ((_a = aggregatedValue.get(rowValue)) !== null && _a !== void 0 ? _a : 0) + 1);
                                return aggregatedValue;
                            },
                            done: (aggregatedValue) => {
                                var _a;
                                const sorted = [...aggregatedValue.entries()].sort(([aVal, aFreq], [bVal, bFreq]) => {
                                    if (aFreq < bFreq) {
                                        return 1;
                                    }
                                    else if (aFreq > bFreq || aVal < bVal) {
                                        return -1;
                                    }
                                    else {
                                        return aVal === bVal ? 0 : 1;
                                    }
                                });
                                return (_a = sorted === null || sorted === void 0 ? void 0 : sorted[0]) === null || _a === void 0 ? void 0 : _a[0];
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            (0, exports.addGroupByParams)(groupByParameter === null || groupByParameter === void 0 ? void 0 : groupByParameter.value, aggregationExpressionEvaluation);
            const result = {
                name: 'MODE',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the mode of the column values\nOptionally the aggregation may be grouped.',
        signatures: [
            'MODE( [colName] )',
            'MODE( COL(name: string))',
            'MODE( [colNameA], GROUP_BY( [colNameB] ))',
            'MODE( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['MODE([colA])', 'MODE([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: [['number'], ['text'], ['date']],
    },
    DISTINCT: {
        handler(args, context) {
            const distinctColumnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('DISTINCT', args);
            const distinctColumnName = distinctColumnParameter.value;
            const groupByParameter = (0, expressionFunctionUtils_1.extractParameter)('MODE', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        DISTINCT: {
                            name: 'DISTINCT',
                            field: distinctColumnName,
                            initialValue: new Set(),
                            reducer: (aggregatedValue, rowValue, rowNode) => {
                                if (typeof rowValue !== 'number' && typeof rowValue !== 'string') {
                                    return aggregatedValue;
                                }
                                aggregatedValue.add(rowValue);
                                return aggregatedValue;
                            },
                            done: (aggregatedValue) => {
                                return aggregatedValue.size;
                            },
                        },
                    },
                },
                getRowNodes: context.getRowNodes,
            };
            (0, exports.addGroupByParams)(groupByParameter === null || groupByParameter === void 0 ? void 0 : groupByParameter.value, aggregationExpressionEvaluation);
            const result = {
                name: 'DISTINCT',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the distinct number of the column values\nOptionally the aggregation may be grouped.',
        signatures: [
            'DISTINCT( [colName] )',
            'DISTINCT( COL(name: string))',
            'DISTINCT( [colNameA], GROUP_BY( [colNameB] ))',
            'DISTINCT( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['DISTINCT([colA])', 'DISTINCT([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: [['number'], ['text'], ['date']],
    },
    ONLY: {
        handler(args, context) {
            const distinctColumnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('ONLY', args);
            const onlyColumnName = distinctColumnParameter.value;
            const columnType = (0, expressionFunctionUtils_1.validateColumnType)(onlyColumnName, ['Number'], 'ONLY', context.adaptableApi);
            const groupByParameter = (0, expressionFunctionUtils_1.extractParameter)('ONLY', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        ONLY: {
                            name: 'ONLY',
                            field: onlyColumnName,
                            initialValue: new Set(),
                            reducer: (aggregatedValue, rowValue, rowNode) => {
                                if (typeof rowValue !== 'number' && typeof rowValue !== 'string') {
                                    return aggregatedValue;
                                }
                                aggregatedValue.add(rowValue);
                                return aggregatedValue;
                            },
                            done: (aggregatedValue) => {
                                return aggregatedValue.size === 1 ? aggregatedValue.values().next().value : null;
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            (0, exports.addGroupByParams)(groupByParameter === null || groupByParameter === void 0 ? void 0 : groupByParameter.value, aggregationExpressionEvaluation);
            const result = {
                name: 'ONLY',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing to a value that is common to all rows. \nOptionally the aggregation may be grouped.',
        signatures: [
            'ONLY( [colName] )',
            'ONLY( COL(name: string))',
            'ONLY( [colNameA], GROUP_BY( [colNameB] ))',
            'ONLY( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['ONLY([colA])', 'ONLY([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: [['number'], ['text'], ['date']],
    },
    STD_DEVIATION: {
        handler(args, context) {
            const columnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('STD_DEVIATION', args);
            const columnName = columnParameter.value;
            const columnType = (0, expressionFunctionUtils_1.validateColumnType)(columnName, ['Number'], 'STD_DEVIATION', context.adaptableApi);
            const groupByParameter = (0, expressionFunctionUtils_1.extractParameter)('STD_DEVIATION', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        STD_DEVIATION: {
                            name: 'STD_DEVIATION',
                            field: columnName,
                            initialValue: { sumOfValues: 0, values: [] },
                            reducer: (aggregatedValue, rowValue, rowNode) => {
                                // TODO: getNumericValue; allow numeric strings
                                if (typeof rowValue !== 'number' || isNaN(rowValue)) {
                                    return aggregatedValue;
                                }
                                aggregatedValue.sumOfValues = aggregatedValue.sumOfValues + rowValue;
                                aggregatedValue.values.push(rowValue);
                                return aggregatedValue;
                            },
                            done: (aggregatedValue) => {
                                const { sumOfValues, values } = aggregatedValue;
                                if (values.length === 0) {
                                    return 0;
                                }
                                const mean = sumOfValues / values.length;
                                const variance = (0, ArrayExtensions_1.SumArray)(values.map((value) => Math.pow(value - mean, 2))) / values.length;
                                return Math.sqrt(variance);
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            (0, exports.addGroupByParams)(groupByParameter === null || groupByParameter === void 0 ? void 0 : groupByParameter.value, aggregationExpressionEvaluation);
            const result = {
                name: 'STD_DEVIATION',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing to a value that is common to all rows. \nOptionally the aggregation may be grouped.',
        signatures: [
            'ONLY( [colName] )',
            'ONLY( COL(name: string))',
            'ONLY( [colNameA], GROUP_BY( [colNameB] ))',
            'ONLY( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['ONLY([colA])', 'ONLY([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: ['number'],
    },
    MIN: {
        handler(args, context) {
            const minColumnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('MIN', args);
            const minColumnName = minColumnParameter.value;
            const columnType = (0, expressionFunctionUtils_1.validateColumnType)(minColumnName, ['Number', 'Date'], 'MIN', context.adaptableApi);
            const groupByParameter = (0, expressionFunctionUtils_1.extractParameter)('MIN', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        MIN: {
                            name: 'MIN',
                            field: minColumnName,
                            initialValue: Number.MAX_VALUE,
                            reducer: (minValue, rowValue) => {
                                if (isUndefinedValue(rowValue)) {
                                    return minValue;
                                }
                                if (minValue === Number.MAX_VALUE) {
                                    return rowValue;
                                }
                                if (columnType === 'Number') {
                                    const numericRowValue = (0, expressionFunctionUtils_1.getNumericValue)(rowValue);
                                    return numericRowValue < minValue ? numericRowValue : minValue;
                                }
                                else {
                                    // Date
                                    return (0, isAfter_1.default)((0, DateHelper_1.parseDateValue)(rowValue), (0, DateHelper_1.parseDateValue)(minValue))
                                        ? minValue
                                        : rowValue;
                                }
                            },
                            done: (minValue) => {
                                if (minValue !== Number.MAX_VALUE) {
                                    return minValue;
                                }
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            (0, exports.addGroupByParams)(groupByParameter === null || groupByParameter === void 0 ? void 0 : groupByParameter.value, aggregationExpressionEvaluation);
            const result = {
                name: 'MIN',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the minimum of the column values\nOptionally the aggregation may be computed within provided individual groups',
        signatures: [
            'MIN( [colName] )',
            'MIN( COL(name: string))',
            'MIN( [colNameA], GROUP_BY( [colNameB] ))',
            'MIN( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['MIN([colA])', 'MIN([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: ['number'],
    },
    MAX: {
        handler(args, context) {
            const maxColumnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('MAX', args);
            const maxColumnName = maxColumnParameter.value;
            const columnType = (0, expressionFunctionUtils_1.validateColumnType)(maxColumnName, ['Number', 'Date'], 'MAX', context.adaptableApi);
            const groupByParameter = (0, expressionFunctionUtils_1.extractParameter)('MAX', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        MAX: {
                            name: 'MAX',
                            field: maxColumnName,
                            initialValue: Number.MIN_VALUE,
                            reducer: (maxValue, rowValue) => {
                                if (isUndefinedValue(rowValue)) {
                                    return maxValue;
                                }
                                if (maxValue === Number.MIN_VALUE) {
                                    return rowValue;
                                }
                                if (columnType === 'Number') {
                                    const numericRowValue = (0, expressionFunctionUtils_1.getNumericValue)(rowValue);
                                    return numericRowValue > maxValue ? numericRowValue : maxValue;
                                }
                                else {
                                    // Date
                                    return (0, isAfter_1.default)((0, DateHelper_1.parseDateValue)(rowValue), (0, DateHelper_1.parseDateValue)(maxValue))
                                        ? rowValue
                                        : maxValue;
                                }
                            },
                            done: (maxValue) => {
                                if (maxValue !== Number.MIN_VALUE) {
                                    return maxValue;
                                }
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
                columnType,
            };
            (0, exports.addGroupByParams)(groupByParameter === null || groupByParameter === void 0 ? void 0 : groupByParameter.value, aggregationExpressionEvaluation);
            const result = {
                name: 'MAX',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Aggregates a column over multiple rows by computing the maximum of the column values\nOptionally the aggregation may be computed within provided individual groups',
        signatures: [
            'MAX( [colName] )',
            'MAX( COL(name: string))',
            'MAX( [colNameA], GROUP_BY( [colNameB] ))',
            'MAX( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['MAX([colA])', 'MAX([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: ['number'],
    },
    QUANT: {
        handler(args, context) {
            var _a, _b, _c;
            const quantileColumnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('QUANT', args);
            const quantileColumnName = quantileColumnParameter.value;
            const columnType = (0, expressionFunctionUtils_1.validateColumnType)(quantileColumnName, ['Number'], 'QUANT', context.adaptableApi);
            const qNumberCandidates = args.filter((arg) => typeof arg === 'number');
            if (qNumberCandidates.length !== 1) {
                throw new ExpressionEvaluationError_1.ExpressionEvaluationError('QUANT', 'expects a single positive numeric argument as q-quantile');
            }
            const qNumber = qNumberCandidates[0];
            const groupByOperand = (0, expressionFunctionUtils_1.extractParameter)('QUANT', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const groupByColumnNames = groupByOperand === null || groupByOperand === void 0 ? void 0 : groupByOperand.value;
            let quantReducer;
            if (groupByColumnNames === null || groupByColumnNames === void 0 ? void 0 : groupByColumnNames.length) {
                // do the heavy-lifting in done()
                quantReducer = {
                    initialValue: {},
                    reducer: (nodeValueMap, rowValue, rowNode, rowIndex) => {
                        if (isUndefinedValue(rowValue)) {
                            return nodeValueMap;
                        }
                        nodeValueMap[rowNode.id] = rowValue;
                        return nodeValueMap;
                    },
                    done: (nodeValueMap, groupItems) => {
                        var _a, _b;
                        const populationSize = (_b = (_a = groupItems === null || groupItems === void 0 ? void 0 : groupItems.filter((rowNode) => {
                            const rowValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, quantileColumnName);
                            return isDefinedValue(rowValue);
                        })) === null || _a === void 0 ? void 0 : _a.length) !== null && _b !== void 0 ? _b : 0;
                        const bucketsMap = new Map();
                        for (let step = 1; step <= qNumber; step++) {
                            bucketsMap.set(step, populationSize * (step / qNumber));
                        }
                        const indexBucketMap = new Map();
                        for (let populationIndex = 0; populationIndex < populationSize; populationIndex++) {
                            for (let quantIndex = 1; quantIndex <= qNumber; quantIndex++) {
                                if (bucketsMap.get(quantIndex) < populationIndex + 1) {
                                    continue;
                                }
                                else {
                                    indexBucketMap.set(populationIndex, quantIndex);
                                    break;
                                }
                            }
                        }
                        const groupedRowBucketMap = {};
                        groupItems.forEach((subgroupRowNode, subgroupRowIndex) => {
                            const subgroupRowValue = nodeValueMap[subgroupRowNode.id];
                            if (subgroupRowValue) {
                                groupedRowBucketMap[subgroupRowValue] = indexBucketMap.get(subgroupRowIndex);
                            }
                        });
                        return groupedRowBucketMap;
                    },
                };
            }
            else {
                // in case of non-grouped quantiles, we are able to calculate the buckets beforehand (which is the most efficient)
                const populationSize = (_c = (_b = (_a = context.adaptableApi.gridApi.getAllRowNodes()) === null || _a === void 0 ? void 0 : _a.filter((rowNode) => {
                    const rowValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, quantileColumnName);
                    return isDefinedValue(rowValue);
                })) === null || _b === void 0 ? void 0 : _b.length) !== null && _c !== void 0 ? _c : 0;
                const bucketsMap = new Map();
                for (let step = 1; step <= qNumber; step++) {
                    bucketsMap.set(step, populationSize * (step / qNumber));
                }
                const indexBucketMap = new Map();
                for (let populationIndex = 0; populationIndex < populationSize; populationIndex++) {
                    for (let quantIndex = 1; quantIndex <= qNumber; quantIndex++) {
                        if (bucketsMap.get(quantIndex) < populationIndex + 1) {
                            continue;
                        }
                        else {
                            indexBucketMap.set(populationIndex, quantIndex);
                            break;
                        }
                    }
                }
                quantReducer = {
                    initialValue: {},
                    reducer: (rowBucketMap, rowValue, rowNode, rowIndex) => {
                        if (isUndefinedValue(rowValue)) {
                            return rowBucketMap;
                        }
                        rowBucketMap[rowValue] = indexBucketMap.get(rowIndex);
                        return rowBucketMap;
                    },
                };
            }
            const aggregationExpressionEvaluation = {
                sortByColumn: quantileColumnName,
                rowFilterFn: (rowNode) => {
                    const rowValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, quantileColumnName);
                    return isDefinedValue(rowValue);
                },
                getRowNodes: context.getRowNodes,
                aggregationParams: {
                    reducers: {
                        QUANT: Object.assign({ name: 'QUANT', field: quantileColumnName }, quantReducer),
                    },
                },
                rowValueGetter: (rowNode, rowBucketMap) => {
                    const rowValue = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, quantileColumnName);
                    if (rowValue == null) {
                        return;
                    }
                    return rowBucketMap[rowValue];
                },
                columnType,
            };
            (0, exports.addGroupByParams)(groupByOperand === null || groupByOperand === void 0 ? void 0 : groupByOperand.value, aggregationExpressionEvaluation);
            const result = {
                name: 'QUANT',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Calculates the q-Quantiles of the given group and displays which q-quantile the given value is in\nCommon quantiles are 4-quantile or Quartile, 5-quantile or Quintile, 10-quantile or Decile and 100-quantile or Percentile',
        signatures: [
            'QUANT( [colName], q: number)',
            'QUANT( COL(name: string), q: number)',
            'QUANT( [colName1], q: number, GROUP_BY([colName2]) )',
        ],
        examples: [
            'QUANT( [col1], 5)',
            `QUANT( COL('col1'), 100))`,
            'QUANT( [col1], 4, GROUP_BY([col2]) )',
        ],
        category: 'aggregation',
    },
    QUARTILE: {
        handler(args, context) {
            const quartileArgs = [...args, 4];
            return exports.aggregatedScalarExpressionFunctions.QUANT.handler(quartileArgs, context);
        },
        description: 'Calculates the Quartiles of the given group and displays which quartile the given value is in',
        signatures: ['QUARTILE( [colName])', 'QUARTILE( [colName1], GROUP_BY([colName2]) )'],
        examples: ['QUARTILE( [col1], 5)', 'QUARTILE( [col1], 4, GROUP_BY([col2]) )'],
        category: 'aggregation',
    },
    PERCENTILE: {
        handler(args, context) {
            const quartileArgs = [...args, 100];
            return exports.aggregatedScalarExpressionFunctions.QUANT.handler(quartileArgs, context);
        },
        description: 'Calculates the Percentile of the given group and displays which percentile the given value is in',
        signatures: ['PERCENTILE( [colName])', 'PERCENTILE( [colName1], GROUP_BY([colName2]) )'],
        examples: ['PERCENTILE( [col1], 5)', 'PERCENTILE( [col1], 4, GROUP_BY([col2]) )'],
        category: 'aggregation',
    },
    COUNT: {
        handler(args, context) {
            const countColumnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('COUNT', args);
            const countColumnName = countColumnParameter.value;
            const groupByParameter = (0, expressionFunctionUtils_1.extractParameter)('COUNT', 'operand', ['GROUP_BY'], args, {
                isOptional: true,
            });
            const aggregationExpressionEvaluation = {
                aggregationParams: {
                    reducers: {
                        COUNT: {
                            name: 'COUNT',
                            field: countColumnName,
                            initialValue: 0,
                            reducer: (totalSum, rowValue) => {
                                if (isUndefinedValue(rowValue)) {
                                    return totalSum;
                                }
                                return totalSum + 1;
                            },
                        },
                    },
                },
                rowFilterFn: context.filterFn,
                getRowNodes: context.getRowNodes,
            };
            (0, exports.addGroupByParams)(groupByParameter === null || groupByParameter === void 0 ? void 0 : groupByParameter.value, aggregationExpressionEvaluation);
            const result = {
                name: 'COUNT',
                type: 'aggregationScalar',
                value: aggregationExpressionEvaluation,
            };
            return result;
        },
        description: 'Counts the number of non-null (i.e. not empty) values in a given column.\nOptionally the aggregation may be computed within provided individual groups',
        signatures: [
            'COUNT( [colName] )',
            'COUNT( COL(name: string))',
            'COUNT( [colNameA], GROUP_BY( [colNameB] ))',
            'COUNT( COL(nameA: string), GROUP_BY( COL(nameB: string)))',
        ],
        examples: ['COUNT([colA])', 'COUNT([colA], GROUP_BY([colB]))'],
        category: 'aggregation',
        inputs: [['number'], ['text'], ['date']],
    },
    OVER: {
        handler(args, context) {
            const columnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('OVER', args);
            (0, expressionFunctionUtils_1.validateColumnType)(columnParameter.value, ['Number', 'Date'], 'OVER', context.adaptableApi);
            const result = {
                type: 'operand',
                name: 'OVER',
                value: columnParameter.value,
            };
            return result;
        },
        description: 'Defines an accumulative dimension (order) for the enclosing cumulative aggregation',
        signatures: ['OVER( [colName] )', 'OVER( COL(name: string))'],
        examples: ['OVER( [colName] )', `OVER( COL('colName'))`, `CUMUL( SUM([colA]), OVER([colB]))`],
        category: 'cumulative',
    },
    WEIGHT: {
        handler(args, context) {
            const columnParameter = (0, expressionFunctionUtils_1.extractColumnParameter)('WEIGHT', args);
            (0, expressionFunctionUtils_1.validateColumnType)(columnParameter.value, ['Number'], 'WEIGHT', context.adaptableApi);
            const result = {
                type: 'operand',
                name: 'WEIGHT',
                value: columnParameter.value,
            };
            return result;
        },
        description: 'Defines a weight for the enclosing AVG(Average) aggregation',
        signatures: ['WEIGHT( [colName] )', 'WEIGHT( COL(name: string))'],
        examples: [
            'WEIGHT( [colName] )',
            `WEIGHT( COL('colName'))`,
            `AVG( [colName1], WEIGHT([colName2]))`,
        ],
        category: 'operand',
    },
    GROUP_BY: {
        handler(args, context) {
            const columnParameters = (0, expressionFunctionUtils_1.extractColumnParameters)('GROUP_BY', args);
            const result = {
                type: 'operand',
                name: 'GROUP_BY',
                value: columnParameters.map((columnParam) => columnParam.value),
            };
            return result;
        },
        description: 'Groups an aggregation operation within the rows that have the same value in the specified column',
        signatures: ['GROUP_BY( [colName] )', 'GROUP_BY( COL(name: string))'],
        examples: ['GROUP_BY( [colName] )', `GROUP_BY( COL('colName'))`],
        category: 'grouping',
    },
    COL: {
        handler(args, context) {
            return (0, expressionFunctionUtils_1.handleColumnFunction)(args, context);
        },
        description: 'References a column by its unique identifier',
        signatures: ['[colName]', 'COL(name: string)'],
        examples: ['[col1]', 'COL("col1")'],
        category: 'special',
    },
};
const mapAggregationToCumulation = (aggregationParameter, overColumnParameter, context) => {
    const aggregationEvaluation = aggregationParameter.value;
    const aggregationReducer = aggregationEvaluation.aggregationParams.reducers[aggregationParameter.name];
    const cumulationAggregationReducer = aggregationParameter.name === 'SUM'
        ? {
            name: 'CUMUL_SUM',
            field: aggregationReducer.field,
            initialValue: {
                currentValue: aggregationReducer.initialValue,
                cumulatedValues: {},
            },
            reducer: (cumulationBag, rowValue, rowNode) => {
                if (isDefinedValue(rowValue)) {
                    cumulationBag.currentValue = cumulationBag.currentValue + rowValue;
                }
                cumulationBag.cumulatedValues[rowNode.id] = cumulationBag.currentValue;
                return cumulationBag;
            },
        }
        : aggregationParameter.name === 'MIN'
            ? {
                name: 'CUMUL_MIN',
                field: aggregationReducer.field,
                initialValue: {
                    currentValue: aggregationReducer.initialValue,
                    cumulatedValues: {},
                },
                reducer: (cumulationBag, rowValue, rowNode) => {
                    if (isDefinedValue(rowValue)) {
                        if (aggregationEvaluation.columnType === 'Date') {
                            if (cumulationBag.currentValue === aggregationReducer.initialValue ||
                                (0, isAfter_1.default)((0, DateHelper_1.parseDateValue)(cumulationBag.currentValue), (0, DateHelper_1.parseDateValue)(rowValue))) {
                                cumulationBag.currentValue = rowValue;
                            }
                        }
                        else if (rowValue < cumulationBag.currentValue) {
                            cumulationBag.currentValue = rowValue;
                        }
                    }
                    if (cumulationBag.currentValue !== aggregationReducer.initialValue) {
                        cumulationBag.cumulatedValues[rowNode.id] = cumulationBag.currentValue;
                    }
                    return cumulationBag;
                },
            }
            : aggregationParameter.name === 'MAX'
                ? {
                    name: 'CUMUL_MAX',
                    field: aggregationReducer.field,
                    initialValue: {
                        currentValue: aggregationReducer.initialValue,
                        cumulatedValues: {},
                    },
                    reducer: (cumulationBag, rowValue, rowNode) => {
                        if (isDefinedValue(rowValue)) {
                            if (aggregationEvaluation.columnType === 'Date') {
                                if (cumulationBag.currentValue === aggregationReducer.initialValue ||
                                    (0, isAfter_1.default)((0, DateHelper_1.parseDateValue)(rowValue), (0, DateHelper_1.parseDateValue)(cumulationBag.currentValue))) {
                                    cumulationBag.currentValue = rowValue;
                                }
                            }
                            else if (rowValue > cumulationBag.currentValue) {
                                cumulationBag.currentValue = rowValue;
                            }
                        }
                        if (cumulationBag.currentValue !== aggregationReducer.initialValue) {
                            cumulationBag.cumulatedValues[rowNode.id] = cumulationBag.currentValue;
                        }
                        return cumulationBag;
                    },
                }
                : aggregationParameter.name === 'AVG'
                    ? {
                        name: 'CUMUL_AVG',
                        field: aggregationReducer.field,
                        initialValue: {
                            currentValue: 0,
                            cumulatedValues: {},
                            numberOfCumulatedValues: 0,
                        },
                        reducer: (cumulationBag, rowValue, rowNode) => {
                            var _a, _b;
                            if (isDefinedValue(rowValue)) {
                                if ((_a = aggregationEvaluation.context) === null || _a === void 0 ? void 0 : _a.weightParam) {
                                    // weighted average
                                    const weightValue = (_b = context.adaptableApi.gridApi.getRawValueFromRowNode(rowNode, aggregationEvaluation.context.weightParam.value)) !== null && _b !== void 0 ? _b : 0;
                                    cumulationBag.currentValue =
                                        cumulationBag.currentValue + rowValue * weightValue;
                                    cumulationBag.numberOfCumulatedValues =
                                        cumulationBag.numberOfCumulatedValues + weightValue;
                                }
                                else {
                                    cumulationBag.currentValue = cumulationBag.currentValue + rowValue;
                                    cumulationBag.numberOfCumulatedValues =
                                        cumulationBag.numberOfCumulatedValues + 1;
                                }
                            }
                            if (cumulationBag.numberOfCumulatedValues !== 0) {
                                cumulationBag.cumulatedValues[rowNode.id] =
                                    cumulationBag.currentValue / cumulationBag.numberOfCumulatedValues;
                            }
                            return cumulationBag;
                        },
                    }
                    : aggregationParameter.name === 'PERCENTAGE'
                        ? {
                            name: 'CUMUL_PERCENTAGE',
                            field: aggregationReducer.field,
                            initialValue: {
                                currentValue: aggregationReducer.initialValue,
                                cumulatedValues: {},
                            },
                            reducer: (cumulationBag, rowValue, rowNode) => {
                                if (isDefinedValue(rowValue)) {
                                    cumulationBag.currentValue = cumulationBag.currentValue + rowValue;
                                }
                                cumulationBag.cumulatedValues[rowNode.id] = cumulationBag.currentValue;
                                return cumulationBag;
                            },
                            done: (cumulationBag) => {
                                cumulationBag.totalAggregationValue = cumulationBag.currentValue;
                                return cumulationBag;
                            },
                        }
                        : aggregationParameter.name === 'COUNT'
                            ? {
                                name: 'CUMUL_COUNT',
                                field: aggregationReducer.field,
                                initialValue: {
                                    currentValue: aggregationReducer.initialValue,
                                    cumulatedValues: {},
                                },
                                reducer: (cumulationBag, rowValue, rowNode) => {
                                    if (isDefinedValue(rowValue)) {
                                        cumulationBag.currentValue = cumulationBag.currentValue + 1;
                                    }
                                    cumulationBag.cumulatedValues[rowNode.id] = cumulationBag.currentValue;
                                    return cumulationBag;
                                },
                            }
                            : null;
    const rowValueGetter = aggregationParameter.name === 'PERCENTAGE'
        ? (rowNode, cumulationBag) => {
            const cumulatedSum = cumulationBag.cumulatedValues[rowNode.id];
            const totalValue = cumulationBag.totalAggregationValue;
            return (cumulatedSum / totalValue) * 100;
        }
        : (rowNode, aggregatedValue) => {
            return aggregatedValue.cumulatedValues[rowNode.id];
        };
    const cumulationExpressionEvaluation = {
        sortByColumn: overColumnParameter.value,
        aggregationParams: {
            reducers: {
                CUMUL: cumulationAggregationReducer,
            },
            groupBy: aggregationEvaluation.aggregationParams.groupBy,
        },
        rowValueGetter,
        rowFilterFn: aggregationEvaluation.rowFilterFn,
        getRowNodes: context.getRowNodes,
    };
    return cumulationExpressionEvaluation;
};
exports.aggregatedScalarExpressionFunctionNames = (0, TypeExtensions_1.getTypedKeys)(exports.aggregatedScalarExpressionFunctions);
// !! mutates expressionEvaluation
const addGroupByParams = (groupByColumnNames, expressionEvaluation) => {
    if (groupByColumnNames === null || groupByColumnNames === void 0 ? void 0 : groupByColumnNames.length) {
        expressionEvaluation.aggregationParams.groupBy = groupByColumnNames.map((columnName) => ({
            field: columnName,
        }));
    }
};
exports.addGroupByParams = addGroupByParams;
const isUndefinedValue = (input) => {
    return input == undefined || (typeof input === 'string' && input.trim() === '');
};
const isDefinedValue = (input) => {
    return !isUndefinedValue(input);
};
