/*!
 * Built by Revolist OU ❤️
 */
import debounce from "lodash/debounce";
import { BasePlugin } from "../base.plugin";
import { getColumnByProp } from "../../utils/column.utils";
import { columnTypes, rowTypes } from "../../store/index";
import { isGrouping } from "../groupingRow/grouping.service";
import { getComparer, getNextOrder, getSortingIndex, hasActiveSorting, sortIndexByItems, } from "./sorting.func";
export * from './sorting.types';
export * from './sorting.func';
export * from './sorting.sign';
function getSortableRowIndexes(indexes, source) {
    return indexes.filter(index => !isGrouping(source[index]));
}
function mergeSortedRowsWithGroups(indexes, source, sortedRows) {
    if (sortedRows.length === indexes.length) {
        return sortedRows;
    }
    let rowIndex = 0;
    return indexes.map(index => {
        if (isGrouping(source[index])) {
            return index;
        }
        return sortedRows[rowIndex++];
    });
}
/**
 * Lifecycle
 * 1. @event `beforesorting` - Triggered when sorting just starts. Nothing has happened yet. This can be triggered from a column or from the source. If the type is from rows, the column will be undefined.
 * 2. @event `beforesourcesortingapply` - Triggered before the sorting data is applied to the data source. You can prevent this event, and the data will not be sorted.
 * 3. @event `beforesortingapply` - Triggered before the sorting data is applied to the data source. You can prevent this event, and the data will not be sorted. This event is only called from a column sorting click.
 * 4. @event `aftersortingapply` - Triggered after sorting has been applied and completed. This event occurs for both row and column sorting.
 *
 * Note: If you prevent an event, it will not proceed to the subsequent steps.
 */
export class SortingPlugin extends BasePlugin {
    constructor(revogrid, providers, config) {
        super(revogrid, providers);
        this.revogrid = revogrid;
        /**
         * Delayed sorting promise registered in the grid render job queue.
         */
        this.sortingPromise = null;
        /**
         * Debounced sorting entry point.
         *
         * Sorting can be requested by column changes, source changes, and header
         * clicks in quick succession, so the actual sort is delayed and coalesced.
         */
        this.postponeSort = debounce((order, comparison, sortingColumns, sortingOrder, ignoreViewportUpdate) => this.runSorting(order, comparison, sortingColumns, sortingOrder, ignoreViewportUpdate), 50);
        this.applySortingConfig(config);
        this.addEventListener('sortingconfigchanged', ({ detail }) => {
            config = detail;
            this.applySortingConfig(detail);
            this.startSorting(this.sorting, this.sortingFunc, this.sortingColumns, this.sortingOrder);
        });
        this.addEventListener('beforeheaderrender', ({ detail, }) => {
            var _a;
            const { data: column } = detail;
            if (column.sortable) {
                detail.data = Object.assign(Object.assign({}, column), { order: (_a = this.sorting) === null || _a === void 0 ? void 0 : _a[column.prop], sortIndex: getSortingIndex(this.sorting, column.prop, this.sortingOrder) });
            }
        });
        this.addEventListener('beforeanysource', ({ detail: { type }, }) => {
            // if sorting was provided - sort data
            if (hasActiveSorting(this.sorting) && this.sortingFunc) {
                const event = this.emit('beforesourcesortingapply', { type, sorting: this.sorting });
                if (event.defaultPrevented) {
                    return;
                }
                this.startSorting(this.sorting, this.sortingFunc, this.sortingColumns, this.sortingOrder);
            }
        });
        this.addEventListener('aftercolumnsset', ({ detail: { order }, }) => {
            // if config provided - do nothing, read from config
            if (config) {
                return;
            }
            const columns = this.providers.column.getColumns();
            const sortingFunc = {};
            const sortingColumns = {};
            const sortingOrder = [];
            const sorting = {};
            for (let prop in order) {
                if (order[prop]) {
                    const column = getColumnByProp(columns, prop);
                    const cmp = getComparer(column, order[prop]);
                    sorting[prop] = order[prop];
                    sortingFunc[prop] = cmp;
                    sortingColumns[prop] = column;
                    sortingOrder.push(prop);
                }
            }
            // set sorting
            this.sorting = hasActiveSorting(sorting) ? sorting : undefined;
            this.sortingFunc = this.sorting ? sortingFunc : undefined;
            this.sortingColumns = this.sorting ? sortingColumns : undefined;
            this.sortingOrder = this.sorting ? sortingOrder : undefined;
        });
        this.addEventListener('beforeheaderclick', (e) => {
            var _a, _b, _c, _d;
            if (e.defaultPrevented) {
                return;
            }
            if (!((_b = (_a = e.detail) === null || _a === void 0 ? void 0 : _a.column) === null || _b === void 0 ? void 0 : _b.sortable)) {
                return;
            }
            this.headerclick(e.detail.column, (_d = (_c = e.detail) === null || _c === void 0 ? void 0 : _c.originalEvent) === null || _d === void 0 ? void 0 : _d.shiftKey);
        });
    }
    /**
     * Creates mutable sorting maps from current state when additive sorting is requested.
     */
    createSortingState(additive) {
        var _a;
        return {
            sorting: additive ? Object.assign({}, this.sorting) : {},
            sortingFunc: additive ? Object.assign({}, this.sortingFunc) : {},
            sortingColumns: additive ? Object.assign({}, this.sortingColumns) : {},
            sortingOrder: additive ? [...((_a = this.sortingOrder) !== null && _a !== void 0 ? _a : [])] : [],
        };
    }
    /**
     * Stores normalized sorting state, clearing inactive empty maps.
     */
    setSortingState({ sorting, sortingFunc, sortingColumns, sortingOrder, }) {
        this.sorting = hasActiveSorting(sorting) ? sorting : undefined;
        this.sortingFunc = this.sorting ? sortingFunc : undefined;
        this.sortingColumns = this.sorting ? sortingColumns : undefined;
        this.sortingOrder = this.sorting ? sortingOrder : undefined;
    }
    /**
     * Adds or replaces a column in a sorting state.
     */
    setColumnSorting(state, prop, order, cmp, column) {
        state.sorting[prop] = order;
        state.sortingFunc[prop] = cmp;
        state.sortingColumns[prop] = column;
        if (!state.sortingOrder.some(sortingProp => String(sortingProp) === String(prop))) {
            state.sortingOrder.push(prop);
        }
    }
    /**
     * Removes a column from a sorting state.
     */
    clearColumnSorting(state, prop) {
        delete state.sorting[prop];
        delete state.sortingFunc[prop];
        delete state.sortingColumns[prop];
        const index = state.sortingOrder.findIndex(sortingProp => String(sortingProp) === String(prop));
        if (index >= 0) {
            state.sortingOrder.splice(index, 1);
        }
    }
    /**
     * Normalizes external sorting configuration into internal order,
     * comparator, and column metadata maps.
     */
    applySortingConfig(cfg) {
        var _a;
        if (!cfg) {
            return;
        }
        const state = this.createSortingState(cfg.additive);
        (_a = cfg.columns) === null || _a === void 0 ? void 0 : _a.forEach(col => {
            if (col.order) {
                this.setColumnSorting(state, col.prop, col.order, getComparer(col, col.order), col);
                return;
            }
            this.clearColumnSorting(state, col.prop);
        });
        this.setSortingState(state);
    }
    startSorting(order, sortingFunc, sortingColumns, sortingOrder, ignoreViewportUpdate) {
        if (!this.sortingPromise) {
            // add job before render
            this.revogrid.jobsBeforeRender.push(new Promise(resolve => {
                this.sortingPromise = resolve;
            }));
        }
        if (typeof sortingColumns === 'boolean') {
            this.postponeSort(order, sortingFunc, undefined, undefined, sortingColumns);
            return;
        }
        this.postponeSort(order, sortingFunc, sortingColumns, sortingOrder, ignoreViewportUpdate);
    }
    /**
     * Applies sorting requested by a sortable header click.
     *
     * @param column - Column that initiated sorting.
     * @param additive - If true, add/remove this column from the current multi-sort state.
     */
    headerclick(column, additive) {
        var _a;
        const columnProp = column.prop;
        let order = getNextOrder((_a = this.sorting) === null || _a === void 0 ? void 0 : _a[columnProp]);
        const beforeEvent = this.emit('beforesorting', { column, order, additive });
        if (beforeEvent.defaultPrevented) {
            return;
        }
        order = beforeEvent.detail.order;
        // apply sort data
        const beforeApplyEvent = this.emit('beforesortingapply', {
            column: beforeEvent.detail.column,
            order,
            additive,
        });
        if (beforeApplyEvent.defaultPrevented) {
            return;
        }
        const cmp = getComparer(beforeApplyEvent.detail.column, beforeApplyEvent.detail.order);
        this.applyHeaderSorting(beforeApplyEvent.detail.column, beforeApplyEvent.detail.additive, order, cmp);
        this.startSorting(this.sorting, this.sortingFunc, this.sortingColumns, this.sortingOrder);
    }
    /**
     * Applies sorting state produced by a header click.
     */
    applyHeaderSorting(column, additive, order, cmp) {
        if (!additive) {
            this.setSortingState(order ? {
                sorting: { [column.prop]: order },
                sortingFunc: { [column.prop]: cmp },
                sortingColumns: { [column.prop]: column },
                sortingOrder: [column.prop],
            } : this.createSortingState());
            return;
        }
        const state = this.createSortingState(true);
        if (order) {
            this.setColumnSorting(state, column.prop, order, cmp, column);
        }
        else {
            this.clearColumnSorting(state, column.prop);
        }
        this.setSortingState(state);
    }
    runSorting(order, comparison, sortingColumns, sortingOrder, ignoreViewportUpdate) {
        var _a, _b;
        if (typeof sortingColumns === 'boolean') {
            this.sort(order, comparison, undefined, undefined, undefined, sortingColumns);
            (_a = this.sortingPromise) === null || _a === void 0 ? void 0 : _a.call(this);
            this.sortingPromise = null;
            return;
        }
        this.sort(order, comparison, sortingColumns, sortingOrder, undefined, ignoreViewportUpdate);
        (_b = this.sortingPromise) === null || _b === void 0 ? void 0 : _b.call(this);
        this.sortingPromise = null;
    }
    sort(sorting, sortingFunc, sortingColumns, sortingOrder, types = rowTypes, ignoreViewportUpdate = false) {
        let activeSortingColumns;
        let activeSortingOrder;
        let activeTypes = types;
        let activeIgnoreViewportUpdate = ignoreViewportUpdate;
        if (Array.isArray(sortingColumns)) {
            activeTypes = sortingColumns;
            activeIgnoreViewportUpdate = typeof sortingOrder === 'boolean' ? sortingOrder : false;
        }
        else {
            activeSortingColumns = sortingColumns;
            activeSortingOrder = Array.isArray(sortingOrder) ? sortingOrder : undefined;
        }
        // if no sorting - reset
        if (!Object.keys(sorting || {}).length) {
            for (let type of activeTypes) {
                const storeService = this.providers.data.stores[type];
                // row data
                const source = storeService.store.get('source');
                // row indexes
                const proxyItems = storeService.store.get('proxyItems');
                // row indexes
                const newItemsOrder = Array.from({ length: source.length }, (_, i) => i); // recover indexes range(0, source.length)
                this.providers.dimension.updateSizesPositionByNewDataIndexes(type, newItemsOrder, proxyItems);
                storeService.setData({ proxyItems: newItemsOrder });
            }
        }
        else {
            for (let type of activeTypes) {
                const storeService = this.providers.data.stores[type];
                // row data
                const source = storeService.store.get('source');
                // row indexes
                const proxyItems = storeService.store.get('proxyItems');
                const sortItems = getSortableRowIndexes(proxyItems, source);
                const sortedItems = sortIndexByItems([...sortItems], source, sortingFunc, sorting, activeSortingColumns, activeSortingOrder);
                const newItemsOrder = mergeSortedRowsWithGroups(proxyItems, source, sortedItems);
                // take row indexes before trim applied and proxy items
                const prevItems = storeService.store.get('items');
                storeService.setData({
                    proxyItems: newItemsOrder,
                });
                // take currently visible row indexes
                const newItems = storeService.store.get('items');
                if (!activeIgnoreViewportUpdate) {
                    this.providers.dimension
                        .updateSizesPositionByNewDataIndexes(type, newItems, prevItems);
                }
            }
        }
        // refresh columns to redraw column headers and show correct icon
        columnTypes.forEach((type) => {
            this.providers.column.dataSources[type].refresh();
        });
        this.emit('aftersortingapply');
    }
}
