import { span } from "../../../core/dom";
import { dict } from "../../../core/util/object";
import { is_nullish } from "../../../core/util/types";
import { assert } from "../../../core/util/assert";
import { Grid as SlickGrid, Group } from "@bokeh/slickgrid";
import { DTINDEX_NAME } from "./definitions";
import { TableDataProvider, DataTableView, DataTable } from "./data_table";
import { ColumnDataSource } from "../../sources/column_data_source";
import { RowAggregator } from "./row_aggregators";
import { Model } from "../../../model";
function groupCellFormatter(_row, _cell, _value, _columnDef, dataContext) {
    const { collapsed, level, title } = dataContext;
    const toggle = span({
        class: `slick-group-toggle ${collapsed ? "collapsed" : "expanded"}`,
        style: { "margin-left": `${level * 15}px` },
    });
    const titleElement = span({
        class: "slick-group-title",
    }, title);
    return `${toggle.outerHTML}${titleElement.outerHTML}`;
}
function indentFormatter(formatter, indent) {
    return (row, cell, value, columnDef, dataContext) => {
        const spacer = span({
            class: "slick-group-toggle",
            style: { "margin-left": `${(indent ?? 0) * 15}px` },
        });
        const formatted = formatter != null ? formatter(row, cell, value, columnDef, dataContext) : `${value}`;
        return `${spacer.outerHTML}${formatted.replace(/^<div/, "<span").replace(/div>$/, "span>")}`;
    };
}
function handleGridClick(event, args) {
    const item = this.getDataItem(args.row);
    if (item instanceof Group && event.target.classList.contains("slick-group-toggle")) {
        if (item.collapsed) {
            this.getData().expandGroup(item.groupingKey);
        }
        else {
            this.getData().collapseGroup(item.groupingKey);
        }
        event.stopImmediatePropagation();
        event.preventDefault();
        this.invalidate();
        this.render();
    }
}
export class GroupingInfo extends Model {
    static __name__ = "GroupingInfo";
    constructor(attrs) {
        super(attrs);
    }
    static {
        this.define(({ Bool, Str, List, Ref }) => ({
            getter: [Str, ""],
            aggregators: [List(Ref(RowAggregator)), []],
            collapsed: [Bool, false],
        }));
    }
    get comparer() {
        return (a, b) => {
            return a.value === b.value ? 0 : a.value > b.value ? 1 : -1;
        };
    }
}
export class DataCubeProvider extends TableDataProvider {
    static __name__ = "DataCubeProvider";
    columns;
    groupingInfos;
    groupingDelimiter;
    toggledGroupsByLevel;
    rows;
    target;
    constructor(source, view, columns, target) {
        super(source, view);
        this.columns = columns;
        this.groupingInfos = [];
        this.groupingDelimiter = ":|:";
        this.target = target;
    }
    setGrouping(groupingInfos) {
        this.groupingInfos = groupingInfos;
        this.toggledGroupsByLevel = groupingInfos.map(() => ({}));
        this.refresh();
    }
    extractGroups(rows, parent_group) {
        const groups = [];
        const groupsByValue = new Map();
        const level = parent_group != null ? parent_group.level + 1 : 0;
        const { comparer, getter } = this.groupingInfos[level];
        for (const row of rows) {
            const column = dict(this.source.data).get(getter);
            assert(column != null);
            const value = column[row];
            let group = groupsByValue.get(value);
            if (group == null) {
                const groupingKey = parent_group != null ? `${parent_group.groupingKey}${this.groupingDelimiter}${value}` : `${value}`;
                group = Object.assign(new Group(), { value, level, groupingKey });
                groups.push(group);
                groupsByValue.set(value, group);
            }
            group.rows.push(row);
        }
        if (level < this.groupingInfos.length - 1) {
            for (const group of groups) {
                group.groups = this.extractGroups(group.rows, group);
            }
        }
        groups.sort(comparer);
        return groups;
    }
    calculateTotals(group, aggregators) {
        const totals = { avg: {}, max: {}, min: {}, sum: {} };
        const data = dict(this.source.data);
        const names = [...data.keys()];
        const items = group.rows.map((i) => {
            return names.reduce((obj, name) => ({ ...obj, [name]: data.get(name)[i] }), {});
        });
        for (const aggregator of aggregators) {
            aggregator.init();
            for (const item of items) {
                aggregator.accumulate(item);
            }
            aggregator.storeResult(totals);
        }
        return totals;
    }
    addTotals(groups, level = 0) {
        const { aggregators, collapsed: groupCollapsed } = this.groupingInfos[level];
        const toggledGroups = this.toggledGroupsByLevel[level];
        for (const group of groups) {
            if (!is_nullish(group.groups)) { // XXX: bad typings
                this.addTotals(group.groups, level + 1);
            }
            if (aggregators.length != 0 && group.rows.length != 0) {
                group.totals = this.calculateTotals(group, aggregators);
            }
            group.collapsed = groupCollapsed !== toggledGroups[group.groupingKey];
            group.title = group.value ? `${group.value}` : "";
        }
    }
    flattenedGroupedRows(groups, level = 0) {
        const rows = [];
        for (const group of groups) {
            rows.push(group);
            if (!group.collapsed) {
                const subRows = !is_nullish(group.groups) // XXX: bad typings
                    ? this.flattenedGroupedRows(group.groups, level + 1)
                    : group.rows;
                rows.push(...subRows);
            }
        }
        return rows;
    }
    refresh() {
        const groups = this.extractGroups(this.view.indices);
        const data = dict(this.source.data);
        const labels = data.get(this.columns[0].field);
        assert(labels != null);
        if (groups.length != 0) {
            this.addTotals(groups);
            this.rows = this.flattenedGroupedRows(groups);
            this.target.data = {
                row_indices: this.rows.map(value => value instanceof Group ? value.rows : value),
                labels: this.rows.map(value => value instanceof Group ? value.title : labels[value]),
            };
        }
    }
    getLength() {
        return this.rows.length;
    }
    getItem(i) {
        const item = this.rows[i];
        const data = dict(this.source.data);
        return item instanceof Group
            ? item
            : [...data.keys()].reduce((obj, name) => ({ ...obj, [name]: data.get(name)[item] }), { [DTINDEX_NAME]: item });
    }
    getItemMetadata(i) {
        const my_item = this.rows[i];
        const columns = this.columns.slice(1);
        const aggregators = my_item instanceof Group
            ? this.groupingInfos[my_item.level].aggregators
            : [];
        function adapter(column) {
            const { field: my_field, formatter } = column;
            const aggregator = aggregators.find(({ field_ }) => field_ === my_field);
            if (aggregator != null) {
                const { key } = aggregator;
                return {
                    formatter(row, cell, _value, columnDef, dataContext) {
                        return formatter != null ? formatter(row, cell, dataContext.totals[key][my_field], columnDef, dataContext) : "";
                    },
                };
            }
            return {};
        }
        return my_item instanceof Group
            ? {
                selectable: false,
                focusable: false,
                cssClasses: "slick-group",
                columns: [{ formatter: groupCellFormatter }, ...columns.map(adapter)],
            }
            : {};
    }
    collapseGroup(grouping_key) {
        const level = grouping_key.split(this.groupingDelimiter).length - 1;
        this.toggledGroupsByLevel[level][grouping_key] = !this.groupingInfos[level].collapsed;
        this.refresh();
    }
    expandGroup(grouping_key) {
        const level = grouping_key.split(this.groupingDelimiter).length - 1;
        this.toggledGroupsByLevel[level][grouping_key] = this.groupingInfos[level].collapsed;
        this.refresh();
    }
}
export class DataCubeView extends DataTableView {
    static __name__ = "DataCubeView";
    _render_table() {
        const options = {
            enableCellNavigation: this.model.selectable !== false,
            enableColumnReorder: false,
            autosizeColsMode: this.autosize,
            multiColumnSort: false,
            editable: this.model.editable,
            autoEdit: this.model.auto_edit,
            rowHeight: this.model.row_height,
        };
        const columns = this.model.columns.map(column => column.toColumn());
        columns[0].formatter = indentFormatter(columns[0].formatter, this.model.grouping.length);
        delete columns[0].editor;
        this.data = new DataCubeProvider(this.model.source, this.model.view, columns, this.model.target);
        this.data.setGrouping(this.model.grouping);
        this.el.style.width = `${this.model.width}px`;
        this.grid = new SlickGrid(this.wrapper_el, this.data, columns, options);
        this.grid.onClick.subscribe(handleGridClick);
    }
}
export class DataCube extends DataTable {
    static __name__ = "DataCube";
    constructor(attrs) {
        super(attrs);
    }
    static {
        this.prototype.default_view = DataCubeView;
        this.define(({ List, Ref }) => ({
            grouping: [List(Ref(GroupingInfo)), []],
            target: [Ref(ColumnDataSource)],
        }));
    }
}
//# sourceMappingURL=data_cube.js.map