/* *
 *
 *  Grid Filtering Controller class
 *
 *  (c) 2020-2026 Highsoft AS
 *
 *  Integration of this software requires a license.
 *  - For commercial use, see www.highcharts.com/license
 *  - For non-commercial, see www.highcharts.com/license-eula
 *
 *
 *  Authors:
 *  - Dawid Draguła
 *
 * */
'use strict';
import FilterModifier from '../../../Data/Modifiers/FilterModifier.js';
import { operatorAliases } from '../Table/Actions/ColumnFiltering/FilteringTypes.js';
import { fireEvent, isString } from '../../../Shared/Utilities.js';
/* *
 *
 *  Class
 *
 * */
/**
 * Class that manages one of the data grid querying types - filtering.
 */
class FilteringController {
    /* *
    *
    *  Constructor
    *
    * */
    /**
     * Constructs the FilteringController instance.
     *
     * @param querying
     * The querying controller instance.
     */
    constructor(querying) {
        /**
         * A map of the filtering conditions for each column.
         */
        this.columnConditions = {};
        this.querying = querying;
    }
    /* *
    *
    *  Functions
    *
    * */
    /**
     * Maps filtering options to the filtering condition.
     *
     * @param columnId
     * Id of the column to filter.
     *
     * @param options
     * Filtering options.
     */
    static mapOptionsToFilter(columnId, options) {
        const condition = options.rule?.operator ?? options.condition;
        let operator;
        if (condition) {
            // TODO: Remove, deprecated.
            // Legacy `before`/`after` → `lessThan`/`greaterThan` aliases.
            const alias = operatorAliases[condition];
            operator = (alias ?? condition);
        }
        const value = options.rule?.value ?? options.value;
        const isStringValue = isString(value);
        const stringifiedValue = isStringValue ? value : '';
        const nonValueConditions = ['empty', 'notEmpty', 'true', 'false'];
        if ((typeof value === 'undefined' ||
            (isStringValue && !stringifiedValue)) && !nonValueConditions.includes(operator ?? '')) {
            return;
        }
        switch (operator) {
            case 'contains':
                return {
                    columnId,
                    operator: 'contains',
                    value: stringifiedValue
                };
            case 'doesNotContain':
                return {
                    operator: 'not',
                    condition: {
                        columnId,
                        operator: 'contains',
                        value: stringifiedValue
                    }
                };
            case 'equals':
                return {
                    columnId,
                    operator: '===',
                    value
                };
            case 'doesNotEqual':
                return {
                    columnId,
                    operator: '!==',
                    value
                };
            case 'beginsWith':
                return {
                    columnId,
                    operator: 'startsWith',
                    value: stringifiedValue
                };
            case 'endsWith':
                return {
                    columnId,
                    operator: 'endsWith',
                    value: stringifiedValue
                };
            case 'greaterThan':
                return {
                    columnId,
                    operator: '>',
                    value
                };
            case 'greaterThanOrEqualTo':
                return {
                    columnId,
                    operator: '>=',
                    value
                };
            case 'lessThan':
                return {
                    columnId,
                    operator: '<',
                    value
                };
            case 'lessThanOrEqualTo':
                return {
                    columnId,
                    operator: '<=',
                    value
                };
            case 'empty':
                return {
                    columnId,
                    operator: 'empty',
                    value
                };
            case 'notEmpty':
                return {
                    operator: 'not',
                    condition: {
                        columnId,
                        operator: 'empty',
                        value
                    }
                };
            case 'true':
                return {
                    columnId,
                    operator: '===',
                    value: true
                };
            case 'false':
                return {
                    columnId,
                    operator: '===',
                    value: false
                };
        }
    }
    /**
     * Compares two serializable filter conditions produced from Grid options.
     *
     * @param left
     * The current filter condition.
     *
     * @param right
     * The next filter condition.
     */
    static filterConditionsEqual(left, right) {
        if (left === right) {
            return true;
        }
        if (!left || !right) {
            return false;
        }
        if (typeof left === 'function' ||
            typeof right === 'function' ||
            left.operator !== right.operator) {
            return false;
        }
        if ('condition' in left || 'condition' in right) {
            return ('condition' in left &&
                'condition' in right &&
                FilteringController.filterConditionsEqual(left.condition, right.condition));
        }
        if ('conditions' in left || 'conditions' in right) {
            return ('conditions' in left &&
                'conditions' in right &&
                left.conditions.length === right.conditions.length &&
                left.conditions.every((condition, index) => FilteringController.filterConditionsEqual(condition, right.conditions[index])));
        }
        return ('columnId' in left &&
            'columnId' in right &&
            left.columnId === right.columnId &&
            left.value === right.value &&
            (('ignoreCase' in left ? left.ignoreCase : void 0) ===
                ('ignoreCase' in right ? right.ignoreCase : void 0)));
    }
    /**
     * Loads filtering options from the data grid options.
     */
    loadOptions() {
        const columnPolicy = this.querying.grid.columnPolicy;
        const newConditions = {};
        const columnIds = columnPolicy.getColumnIds();
        for (let i = 0, iEnd = columnIds.length; i < iEnd; ++i) {
            const columnId = columnIds[i];
            if (columnPolicy.isColumnUnbound(columnId)) {
                continue;
            }
            const sourceColumnId = columnPolicy.getColumnSourceId(columnId);
            const filteringOptions = columnPolicy
                .getIndividualColumnOptions(columnId)
                ?.filtering;
            if (!filteringOptions ||
                !sourceColumnId) {
                continue;
            }
            const condition = this.createColumnCondition(columnId, sourceColumnId, filteringOptions);
            if (condition) {
                newConditions[columnId] = condition;
            }
        }
        this.columnConditions = newConditions;
        this.updateModifier();
    }
    /**
     * Adds a new filtering condition to the specified column.
     *
     * @param columnId
     * The column ID to filter in.
     *
     * @param options
     * The filtering options.
     */
    addColumnFilterCondition(columnId, options) {
        if (this.querying.grid.columnPolicy.isColumnUnbound(columnId)) {
            return;
        }
        const sourceColumnId = this.querying.grid
            .columnPolicy.getColumnSourceId(columnId);
        if (!sourceColumnId) {
            return;
        }
        const condition = this.createColumnCondition(columnId, sourceColumnId, options);
        if (condition) {
            this.columnConditions[columnId] = condition;
        }
        else {
            delete this.columnConditions[columnId];
        }
        this.updateModifier();
    }
    /**
     * Clears the filtering condition for the specified column. If no column ID
     * is provided, clears all the column filtering conditions.
     *
     * @param columnId
     * The column ID to clear or `undefined` to clear all the column filtering
     * conditions.
     */
    clearColumnFiltering(columnId) {
        if (!columnId) {
            this.columnConditions = {};
        }
        else {
            delete this.columnConditions[columnId];
        }
        this.updateModifier();
    }
    /**
     * Builds the filter condition for a column, letting data projection
     * features redirect it to the columns actually backing the data.
     *
     * @param columnId
     * Grid column id.
     *
     * @param sourceColumnId
     * Source column id resolved for the Grid column.
     *
     * @param options
     * Filtering options of the column.
     */
    createColumnCondition(columnId, sourceColumnId, options) {
        const e = {
            columnId,
            condition: FilteringController.mapOptionsToFilter(sourceColumnId, options),
            options,
            sourceColumnId
        };
        fireEvent(this.querying.grid, 'resolveFilterCondition', e);
        return e.condition;
    }
    /**
     * Updates the modifier based on the current column conditions.
     */
    updateModifier() {
        const columnConditions = Object.values(this.columnConditions);
        this.querying.shouldBeUpdated = true;
        if (columnConditions.length < 1) {
            delete this.modifier;
            return;
        }
        this.modifier = new FilterModifier({
            condition: {
                operator: 'and',
                conditions: columnConditions
            }
        });
    }
}
/* *
 *
 *  Default Export
 *
 * */
export default FilteringController;
