import { action, computed, observable } from "mobx";
import * as CssSelectorGenerator from "css-selector-generator";
import * as groupBy from "lodash.groupby";

import { GroupModel } from "./GroupModel";
import { ColumnEvent, ColumnModel } from "./ColumnModel";
import { RowEvent, RowModel } from "./RowModel";
import { GroupEvent } from "./GroupModel";
import { CellModel } from "./CellModel";
import { ControlModel } from "@dewesoft-web/ui/controls";
import { DSMessage, WebControl, EventArguments, ds } from "@dewesoft-web/ui/events";
import { Column, RowData, Row, SortDirecton } from "../types";
import { CommonEvent } from "../../ui/events/webEvent";
import { BufferedEvent } from "../../ui/events/ds";
import { GridProps } from "../FlexGrid";

export enum GridEvent {
    Initializing,
    ColumnChanged,
    GroupsChanged,
    GroupChanged,
    RowChanged,
    SelectedColumnChanged,
    ShowGroupsChanged,
    RowOrderChanged,
    GroupByChanged,
    UnselectRows,
    SelectRows,
    SelectRow,
    RemoveRow,
    AddColumn,
    AddRow,
    Clear,
    AddGroup,
    RemoveGroup,
    AddRows,
    SelectedRowsChanged,
    InsertRow,
    SortRows,
    UnsortRows,
    SortedColumnChanged,
    RemoveColumn,
    FilterChanged
}

const ENTER = 13;
const ESC = 27;

@DSMessage(["dsGrid", "onEvent"])
export class GridModel extends ControlModel<GridEvent> {

    element : HTMLDivElement;

    private rows : RowModel[];

    @observable
    groups : GroupModel[];

    @observable
    columns : ColumnModel[];

    @observable
    scrollTop : number;

    @observable
    clientHeight: number;

    @observable
    scrollHeight : number;

    @observable
    scrollWidth : number;

    @observable
    rowHeight: number;

    groupByColumn : string;

    @observable
    filter : string;

    sortDirection : SortDirecton;
    sortedColumn : string;

    groupRows : boolean;

    @observable
    unvirtualizedHeight : number;

    private isBatch : boolean;

    readonly virtualize : boolean;

    private selectedColumn : ColumnModel;
    private selectedColumnIndex : number;

    private selectedRows : RowModel[];
    private editingCell : CellModel;

    private selectionStart : number;
    private selectionEnd : number;

    private isSelecting : boolean;
    private prevScrollTop : number;
    private prevClientHeight : number;
    readonly selector : string;

    private readonly groupTitleHeight : number = 25;

    private readonly headersHeight : number = 42;

    constructor(name : string, options : GridProps, rows? : RowData[], cols? : Column[]) {
        super(WebControl.Grid, name, options);

        this.isBatch = false;

        this.rowHeight = parseInt(options.RowHeight) || 25;

        this.virtualize = true;
        if (options.Virtualize !== undefined && (options.Virtualize as any) !== true) {
            const type = typeof options.Virtualize;
            if (type === "boolean") {
                this.virtualize = options.Virtualize as any as boolean;
            }
            else if (typeof options.Virtualize === "string") {
                this.virtualize = options.Virtualize !== "False";
            }
        }

        if (!cols) {
            this.columns = [];
        }
        else {
            const len = cols.length;
            this.columns = new Array(len);

            let hasGroupName = false;

            for (let i = 0; i < len; i++) {
                this.columns[i] = new ColumnModel(cols[i], this);
                this.columns[i].order = i;

                if (!hasGroupName && this.columns[i].property === "GroupName") {
                    hasGroupName = true;
                }
            }

            if (!hasGroupName) {
                let groupColumn = new ColumnModel({
                    Title: "Group name",
                    Property: "GroupName",
                    Visible: false,
                    ReadOnly: true,
                    Type: "String",
                    Description: "Group name"
                }, this);

                groupColumn.order = this.columns.length;
                this.columns.push(groupColumn);
            }
        }

        if (!rows) {
            this.rows = [];
        }
        else {
            const len = rows.length;
            this.rows = new Array(len);

            for (let i = 0; i < len; i++) {
                this.rows[i] = new RowModel(
                    i,
                    i,
                    {
                        readOnly: false,
                        data: rows[i],
                        visible: true
                    },
                    this.columns,
                    this.virtualize
                );
            }
        }

        this.groupRows = true;
        this.sortDirection = SortDirecton.None;
        this.groupByColumn = "GroupName";
        // console.log("done generating");
        this.selectedRows = [];
        this.selectionStart = -1;
        this.selectionEnd = -1;
        this.isSelecting = false;

        this.prevScrollTop = 0;
        this.prevClientHeight = 0;

        this.onGroupEvent = this.onGroupEvent.bind(this);
        this.onRowEvent = this.onRowEvent.bind(this);
        this.onEvent = this.onEvent.bind(this);

        this.rows.forEach(row => row.addEventHandler(this.onRowEvent));

        const groups = groupBy(this.rows, (r) => {
            return r.cells["GroupName"].data;
        });

        let index = 0;
        this.groups = [];
        // noinspection TsLint
        for (const group in groups) {
            // noinspection JSUnfilteredForInLoop

            this.groups.push(new GroupModel(
                group, index++, groups[group], this.onGroupEvent
            ));
        }

        this.updateDisplayIndex();


        if (!this.virtualize) {
            this.unvirtualizedHeight = this.getUnvirtualizedHeight();
        }
        // this.setScrollPosition(0, 500);
    }

    onInitialized() {
        // console.log(JSON.stringify(initArgs));

        this.triggerEvent(GridEvent.Initializing, {
            rows: this.rows.map(r => r.serialize()),
            columns: this.columns.map(c => c.serialize()),
            groups: this.groups.map(g => g.serialize()),
            groupByColumn: "GroupName"
        });
    }

    setElement(el) {
        (this as any).selector = new CssSelectorGenerator().getSelector(el);
        this.element = el;

        const height = this.getElementHeight(el, true);
        this.clientHeight = height;

        // this.element.style.height = height + "px";
        this.setScrollPosition(0, height);

        // this.setScrollPosition(0, el.clientHeight || 500);
    }

    getElementHeight(el : HTMLElement, parentHeightOnly : boolean = false) : number {
        let size = 500;
        if (!parentHeightOnly && el.clientHeight) {
            size = el.clientHeight;
        }
        else {
            size = this.getParentHeight(el.parentElement);
            if (size <= 0) {
                size = 500;
            }

            size -= this.element.offsetTop;
        }
        return size;
    }

    getParentHeight(el : HTMLElement) : number {
        let size = -1;

        if (el.parentElement) {
            size = el.parentElement.clientHeight;

            if (size === 0) {
                size = this.getParentHeight(el.parentElement);
            }
        }
        return size;
    }

    getIsBatch() : boolean {
        return this.isBatch;
    }

    getRowCount() : number {
        return this.rows.length;
    }



    @action
    setFilter(filter : string, event : boolean = true) {
        this.filter = filter.toLowerCase();

        if (event) {
            this.triggerEvent(GridEvent.FilterChanged, {
                filter: filter
            });
        }

        // console.group("filter");

        for (const row of this.rows) {
            row.contains(this.filter);
        }

        // console.groupEnd();
    }

    @action
    addColumn(column : Column, emitEvent : boolean) {
        const gridColumn = new ColumnModel(column, this);

        if (!gridColumn.order && gridColumn.order !== 0) {
            gridColumn.order = this.columns.length - 1;
        }

        this.columns.push(gridColumn);

        for (const row of this.rows) {
            row.addColumnData();
        }

        if (emitEvent) {
            this.triggerEvent(GridEvent.AddColumn, column);
        }
    }

    @action
    removeColumn(columnProperty : string, emitEvent : boolean) {
        const columnIndex = this.columns.findIndex(c => c.property === columnProperty);

        if (columnIndex !== -1) {
            this.columns.splice(columnIndex, 1);

            for (const row of this.rows) {
                row.removeColumnData(columnProperty);
            }
        }
    }

    private onGroupEvent(control : GroupModel, event : GroupEvent, args : EventArguments, onSuccess? : Function, onFailure? : Function) {
        switch (event) {
            case GroupEvent.ExpandedChanged:
                this.setScrollPosition(this.prevScrollTop, this.prevClientHeight);
                break;
            default:
                break;
        }

        this.triggerEvent(GridEvent.GroupsChanged, {
            groupEvent: event,
            group: control.name,
            arguments: args
        });
    }

    private onRowEvent(control : RowModel, event : RowEvent, args : EventArguments, onSuccess? : Function, onFailure? : Function) {
        // console.log(`RowData[${control.index}] ${RowEvent[event]} => ${JSON.stringify(args)}`);

        this.triggerEvent(GridEvent.RowChanged, {
            rowEvent: event,
            index: control.index,
            arguments: args
        });
    }

    @computed
    get orderedColumns() {
        return this.columns.sort(function(lhs, rhs) {
            return lhs.order - rhs.order;
        });
    }

    toggleGroups() {
        this.showGroups(!this.groupRows);
    }

    showGroups(visible : boolean, noEvent : boolean = false) {

        for (const group of this.groups) {
            group.setTitleVisibility(visible);
        }

        this.groupRows = visible;

        if (!noEvent) {
            this.triggerEvent(GridEvent.ShowGroupsChanged, {
                visible: visible
            });
        }
    }

    sort(column : string, event : boolean = true) {
        if (column !== this.sortedColumn) {
            this.sortedColumn = column;
            this.sortDirection = SortDirecton.Ascending;
        }
        else {
            if (this.sortDirection === SortDirecton.None) {
                this.sortDirection = SortDirecton.Ascending;
            }
            else if (this.sortDirection === SortDirecton.Ascending) {
                this.sortDirection = SortDirecton.Descending;
            }
            else {
                this.sortDirection = SortDirecton.Ascending;
            }
        }

        for (const group of this.groups) {
            group.sort(this.sortDirection === SortDirecton.Ascending, this.sortedColumn);
        }

        this.updateDisplayIndex();

        if (event) {
            this.triggerEvent(GridEvent.SortRows, {
                direction: this.sortDirection
            });
        }
    }

    unsort(event : boolean = true) {
        if (this.sortDirection === SortDirecton.None) {
            return;
        }

        this.sortDirection = SortDirecton.None;
        for (const group of this.groups) {
            group.unsort();
        }

        this.updateDisplayIndex();

        if (event) {
            this.triggerEvent(GridEvent.UnsortRows);
        }
    }

    @action
    selectColumn(col : ColumnModel) {
        if (this.selectedColumn) {
            this.selectedColumn.selected = false;
        }
        this.selectedColumn = col;
        this.selectedColumn.selected = true;

        this.selectedColumnIndex = this.columns.indexOf(this.selectedColumn);
    }

    @action
    startSelectRow(row : RowModel, col : ColumnModel) {
        if (!this.editingCell) {
            this.editingCell = row.edit(col);
        }

        if (this.editingCell.dirty) {
            this.setValueForSelected(this.editingCell.temp);
        }

        this.isSelecting = true;

        this.selectColumn(col);

        this.unselectRows(row.displayIndex);
        this.selectedRows = [row];

        this.selectionStart = row.displayIndex; // this.rows.indexOf(row);
        this.selectionEnd = row.displayIndex; // this.rows.indexOf(row);

        row.select();
    }

    @action
    stopSelecting() {
        this.isSelecting = false;
    }

    @action
    onTrySelect(row : RowModel) {
        if (!this.isSelecting || this.selectedColumn.readOnly || !this.selectedColumn.groupSelect) {
            return;
        }

        ds.ignoreOutboundEvents(true);

        const numRows = this.rows.length;
        const selected = new Array(numRows);

        for (let i = 0; i < numRows; i++) {
            selected[i] = this.rows[i].selected;
        }

        // console.log("onTrySelect");

        this.selectionEnd = row.displayIndex; // this.rows.indexOf(row);

        if (this.selectedRows.length) {
            this.unselectRows(this.selectionEnd);
        }
        this.selectRows(this.selectionStart, this.selectionEnd);

        if (this.selectedRows.length) {
            this.selectedRows[0].first = true;
            this.selectedRows[this.selectedRows.length - 1].last = true;
        }

        ds.ignoreOutboundEvents(false);

        for (let i = 0; i < numRows; i++) {
            if (this.rows[i].selected !== selected[i]) {
                this.rows[i].triggerSelected();
            }
        }

        ds.ignoreOutboundEvents(false);

        for (let i = 0; i < numRows; i++) {
            if (this.rows[i].selected !== selected[i]) {
                this.rows[i].triggerSelected();
            }
        }
    }

    @action
    selectRows(startIndex : number, endIndex : number) {
        this.selectedRows = [];

        if (endIndex < startIndex) {
            let temp = endIndex;

            endIndex = startIndex;
            startIndex = temp;
        }

        const numGroups = this.groups.length;
        for (let index = 0; index < numGroups; index++) {
            const group = this.groups[index];
            const groupRows = group.rows.length;

            for (let rowIndex = 0; rowIndex < groupRows; rowIndex++) {
                const row = group.rows[rowIndex];

                const select = (row.displayIndex >= startIndex) && (row.displayIndex <= endIndex);

                if (select) {
                    row.setSelected(select);
                    row.first = false;
                    row.last = false;

                    this.selectedRows.push(row);
                }
                else {
                    row.deselect();
                }
            }
        }
    }

    @action
    onRowSelect(row : RowModel, col : ColumnModel, selectToRow? : boolean) {
        if (col !== this.selectedColumn || col.readOnly) {
            return;
        }

        if (row.selected) {
            if (this.selectedRows.length === 1) {
                return;
            }

            const index = this.selectedRows.indexOf(row);

            if (index !== -1) {
                this.selectedRows.splice(index, 1);
                row.deselect();
            }
        }
        else if (selectToRow) {
            const index = row.displayIndex; // this.rows.indexOf(row);

            this.selectRows(this.selectionStart, index);

            if (this.selectedRows.length) {
                this.selectedRows[0].first = true;
                this.selectedRows[this.selectedRows.length - 1].last = true;
            }
        }
        else {
            this.selectedRows.push(row);
            row.select();
        }
    }

    @action
    unselectRows(index : number) {
        this.cancelEdit();

        for (const row of this.selectedRows) {
            if (row.displayIndex !== index) {
                row.deselect();
            }
        }
    }

    @action
    selectAllRows(column : ColumnModel) {
        this.cancelEdit();

        this.selectColumn(column);

        if (!this.rows || this.rows.length === 0 || !column.groupSelect) {
            return;
        }

        const length = this.rows.length;

        for (let i = 0; i < length; i++) {
            this.rows[i].selectCustom(i === 0, i === length - 1);
        }

        this.selectedRows = this.rows;
    }

    @action
    setScrollPosition(scrollTop : number, clientHeight : number) {
        // console.group(`scroll / resize (${this.name}), clientHeight: ${clientHeight}`);
        // console.log(`PREV: top: ${this.scrollTop}, height: ${this.scrollHeight}`);

        if (!this.virtualize) {
            return;
        }

        this.clientHeight = clientHeight;

        this.scrollTop = scrollTop + 24;

        let rowPos = 30;
        let rowIdx = 0;

        for (const group of this.groups) {
            if (!group.expanded && group.showTitle) {
                continue;
            }
            else if (group.showTitle) {
                rowPos += this.groupTitleHeight;
            }

            for (const row of group.rows) {
                row.scrollVisible = (rowPos > scrollTop) && (rowPos < scrollTop + clientHeight);

                rowIdx++;
                rowPos += this.rowHeight;
            }
        }

        this.scrollHeight = rowPos + (30 + this.groups.length * 30);

        this.prevScrollTop = scrollTop;
        this.prevClientHeight = clientHeight;

        // console.log(`POST: top: ${this.scrollTop}, height: ${this.scrollHeight}`);
        // console.groupEnd();
    }

    @action
    selectPreviousRow() {
        if (this.selectedRows.length === 0) {
            return;
        }

        if (this.selectionStart > 0) {
            this.cancelEdit();

            this.selectionStart--;
            this.selectionEnd = this.selectionStart;

            this.unselectRows(this.selectionStart);

            // const row = this.rows[this.selectionStart];
            const row = this.rows.find(r => r.displayIndex === this.selectionStart);

            if (row) {
                this.selectedRows = [row];
                row.select();

                // if (this.element) {
                //     this.element.scrollTop = (row.index * 25) - 30;
                // }
            }
        }
    }

    @action
    selectNextRow() {
        if (this.selectedRows.length === 0) {
            return;
        }

        if (this.selectionStart < this.rows.length - 1) {
            this.cancelEdit();

            this.selectionStart++;
            this.selectionEnd = this.selectionStart;

            this.unselectRows(this.selectionStart);

            let row : RowModel;

            const numRows = this.rows.length;
            for (let i = 0; i < numRows; i++) {
                if (this.rows[i].displayIndex === this.selectionStart) {
                    row = this.rows[i];
                    break;
                }
            }

            if (row) {
                this.selectedRows = [row];
                row.select();
            }
        }
    }

    @action
    selectFirstRow() {
        this.cancelEdit();

        this.selectionStart = 0;
        this.selectionEnd = 0;

        if (this.selectedRows.length) {
            this.unselectRows(0);
        }

        this.selectRows(this.selectionStart, this.selectionEnd);

        if (this.selector && window.document) {
            const el : Element = document.querySelector(this.selector);
            el.scrollTop = 0;
        }

        this.selectedRows[0].last = true;
    }

    selectLastRow() {
        this.cancelEdit();

        this.selectionStart = this.rows.length - 1;
        this.selectionEnd = this.rows.length - 1;

        if (this.selectedRows.length) {
            this.unselectRows(this.selectionStart);
        }
        this.selectRows(this.selectionStart, this.selectionEnd);

        if (this.selector && window.document) {
            const el : Element = document.querySelector(this.selector);
            el.scrollTop = el.scrollHeight + (3 * 25);
        }

        this.selectedRows[0].first = true;
    }

    selectNextColumn() {
        if (!this.selectedColumn || this.selectedColumnIndex >= this.columns.length) {
            return;
        }

        this.cancelEdit();

        let selectedIndex = this.selectedColumnIndex + 1;

        while (selectedIndex < this.columns.length && !this.columns[selectedIndex].visible) {
            selectedIndex++;
        }

        const col = this.columns[selectedIndex];
        if (col) {
            this.selectedColumn.selected = false;

            this.selectedColumn = col;
            this.selectedColumn.selected = true;
            this.selectedColumnIndex = selectedIndex;
        }
    }

    selectPreviousColumn() {
        if (!this.selectedColumn || this.selectedColumnIndex <= 0) {
            return;
        }

        this.cancelEdit();

        let selectedIndex = this.selectedColumnIndex - 1;

        while (selectedIndex >= 0 && !this.columns[selectedIndex].visible) {
            selectedIndex--;
        }

        const col = this.columns[selectedIndex];
        if (col) {
            this.selectedColumn.selected = false;

            this.selectedColumn = col;
            this.selectedColumn.selected = true;
            this.selectedColumnIndex = selectedIndex;
        }
    }

    cancelEdit() {
        if (this.editingCell) {
            this.editingCell.stopEditing();
        }
    }

    onEditCell(row : RowModel, col : ColumnModel) {
        this.cancelEdit();

        this.editingCell = row.edit(col);
    }

    onStartEdit(cell : CellModel) {
        this.cancelEdit();

        this.editingCell = cell;

        this.editingCell.startEditing();
    }

    setValueForSelected(value : any) : void {
        if (!this.editingCell) {
            return;
        }
        else if (this.editingCell.isReadOnly) {
            return;
        }

        for (const row of this.selectedRows) {
            row.setValue(value, this.selectedColumn);
        }

        this.editingCell.stopEditing();
    }

    setGroupBy(order : any) {
        const col = this.columns.find(function(column) {
            return column.order === order;
        });

        if (col) {

        }
    }

    @action
    addGroup(group : GroupModel, noEvent : boolean = false) {
        group.setTitleVisibility(this.groupRows);

        this.groups.push(group);

        if (!noEvent) {
            this.triggerEvent(GridEvent.AddGroup, {
                group: group.serialize()
            });
        }
    }

    @action
    removeGroup(group : GroupModel) {
        const index = this.groups.indexOf(group);

        if (index !== -1) {
            this.groups.splice(index, 1);

            this.triggerEvent(GridEvent.RemoveGroup, {
                groupName: group.name
            });
        }
    }

    private updateRowOrder() {
        if (this.sortDirection !== SortDirecton.None) {
            if (this.sortDirection === SortDirecton.Ascending) {
                this.sortDirection = SortDirecton.Descending;
            }
            else if (this.sortDirection === SortDirecton.Descending) {
                this.sortDirection = SortDirecton.Ascending;
            }
            this.sort(this.sortedColumn, true);
        }
        else {
            this.updateDisplayIndex();
        }
    }

    private updateDisplayIndex() {
        let index = 0;
        for (const group of this.groups) {
            for (let row of group.rows) {
                row.displayIndex = index++;
            }
        }
    }

    @action
    insertRow(row : Row, index : number, noEvent? : boolean, noRefresh? : boolean) : RowModel {
        let groupToAdd : GroupModel = null;

        for (const group of this.groups) {
            if (group.name === row.data.GroupName) {
                groupToAdd = group;
            }
        }

        if (!groupToAdd) {
            groupToAdd = new GroupModel(row.data.GroupName as string, this.groups.length, [], this.onGroupEvent);

            this.addGroup(groupToAdd, noEvent);
        }

        const rowModel = new RowModel(this.rows.length, 0, row, this.columns, this.virtualize);
        rowModel.addEventHandler(this.onRowEvent);

        this.rows.splice(index, 0, rowModel);
        groupToAdd.rows.push(rowModel);

        // this.updateIndices();
        if (!this.isBatch) {
            this.updateRowOrder();
        }

        if (!noRefresh && !this.isBatch) {
            this.refresh();
        }

        if (!noEvent) {
            this.triggerEvent(GridEvent.AddRow, {
                row: rowModel.serialize()
            });
        }

        if (!this.virtualize) {
            this.unvirtualizedHeight = this.getUnvirtualizedHeight();
        }

        return rowModel;
    }

    @action
    insertRowToGroup(row : Row, groupIndex : number, noEvent? : boolean, noRefresh? : boolean) : RowModel {
        let groupToAdd : GroupModel = null;

        for (const group of this.groups) {
            if (group.name === row.data.GroupName) {
                groupToAdd = group;
            }
        }

        if (!groupToAdd) {
            groupToAdd = new GroupModel(row.data.GroupName as string, this.groups.length, [], this.onGroupEvent);

            this.addGroup(groupToAdd, noEvent);
        }

        const rowModel = new RowModel(this.rows.length, 0, row, this.columns, this.virtualize);
        rowModel.addEventHandler(this.onRowEvent);

        this.rows.push(rowModel);
        groupToAdd.insertRow(groupIndex, rowModel);

        // this.updateIndices();
        if (!this.isBatch) {
            this.updateRowOrder();
        }

        if (!noRefresh && !this.isBatch) {
            this.setScrollPosition(this.prevScrollTop, this.prevClientHeight);
        }

        if (!noEvent) {
            this.triggerEvent(GridEvent.AddRow, {
                row: rowModel.serialize()
            });
        }

        if (!this.virtualize) {
            this.unvirtualizedHeight = this.getUnvirtualizedHeight();
        }

        return rowModel;
    }

    getUnvirtualizedHeight() : number {
        let height = (this.rowHeight * this.rows.length) + this.headersHeight;

        let numShownGroups = 0;
        for (const group of this.groups) {
            if (group.showTitle) {
                height += this.groupTitleHeight;

                numShownGroups++;
            }
        }

        return height;
    }

    @action
    addRow(row : Row, noEvent : boolean = false, noRefresh : boolean = false, lastRowToAdd : boolean = true) : RowModel {
        let groupToAdd : GroupModel = null;

        for (const group of this.groups) {
            if (group.name === row.data.GroupName) {
                groupToAdd = group;
            }
        }

        if (!groupToAdd) {
            groupToAdd = new GroupModel(row.data.GroupName as string, this.groups.length, [], this.onGroupEvent);

            this.addGroup(groupToAdd, noEvent);
        }

        const rowModel = new RowModel(this.rows.length, 0, row, this.columns, this.virtualize);
        rowModel.addEventHandler(this.onRowEvent);

        this.rows.push(rowModel);
        groupToAdd.rows.push(rowModel);

        if (lastRowToAdd && !this.isBatch) {
            this.updateRowOrder();
        }

        if (!noRefresh && !this.isBatch) {
            this.setScrollPosition(this.prevScrollTop, this.prevClientHeight);
        }

        if (!noEvent) {
            this.triggerEvent(GridEvent.AddRow, {
                row: rowModel.serialize()
            });
        }

        if (!this.virtualize) {
            this.unvirtualizedHeight = this.getUnvirtualizedHeight();
        }

        return rowModel;
    }

    @action
    addRows(rows : Row[]) : void {
        const newRows = [];

        let isLastRow = false;
        for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
            isLastRow = rowIndex === rows.length - 1;
            newRows.push(this.addRow(rows[rowIndex], true, true, isLastRow));
        }
        this.refresh();


        // this.triggerEvent(GridEvent.AddRows, {
        //     rows: newRows.map(r => r.serialize())
        // });
    }

    @action
    removeRow(index : number, noEvent? : boolean) : void {
        const row = this.rows[index];

        if (!row) {
            return;
        }

        let targetGroup = this.groups.find(g => g.name === row.cells["GroupName"].data);

        if (!targetGroup) {
            return;
        }

        targetGroup.removeRow(row);
        this.rows.splice(index, 1);

        if (targetGroup.rows.length === 0) {
            this.removeGroup(targetGroup);
        }

        if (!this.isBatch) {
            this.updateIndices();
            this.updateRowOrder();
        }

        if (!noEvent) {
            this.triggerEvent(GridEvent.RemoveRow, {
                removedIds: [{
                    index: index
                }]
            });
        }

        this.refresh();
    }

    private updateIndices() : void {
        const rowsLength = this.rows.length;
        for (let i = 0; i < rowsLength; i++) {
            this.rows[i].index = i;
        }
    }

    @action
    refresh() : void {
        this.setScrollPosition(this.prevScrollTop, this.prevClientHeight);
    }

    onKeyPressed(e) {
        switch (e.keyCode) {
            case ESC:
            case ENTER:
                if (this.editingCell && this.editingCell.type.kind.toLowerCase() === "color") {
                    this.editingCell.stopEditing(true);
                }
                break;
            default:
                break;
        }
    }

    clear() : void {
        this.selectedRows = [];
        this.rows = [];
        this.groups = [];
        this.editingCell = null;

        this.setScrollPosition(0, this.prevClientHeight);
    }

    private onGroupsChanged(name : string, event : GroupEvent, args : any[]) : void {
        const group = this.groups.find(g => g.name === name);

        if (group) {
            group.handleEvent(event, args);
        }
        else {
            // console.warn(`No group with name ${name} found`);
        }
    }

    private onRowChanged(rowIndex : number, rowEvent : RowEvent, rowArgs : any[]) : void {
        if (rowIndex < 0 || rowIndex > this.rows.length) {
            console.warn(`No row with index ${rowIndex} found`);
            return;
        }
        else {
            const row = this.rows[rowIndex];
            if (row !== undefined && row.handleEvent) {
                row.handleEvent(rowEvent, rowArgs);
            }
        }
    }

    private onColumnChanged(property : string, colEvent : ColumnEvent, colArgs : any[]) : void {
        // console.log(`ColChanged[${ColumnEvent[colEvent]} colArgs: ${JSON.stringify(colArgs)}`);

        const col = this.columns.find(c => c.property === property);
        if (col) {
            col.handleEvent(colEvent, colArgs);
        }
        else {
            console.warn(`No column with property ${col.property} found!`);
        }
    }

    protected setOptions(options : any) {
    }

    private beginUpdate() {
        this.isBatch = true;
    }

    private endUpdate() {
        this.isBatch = false;
    }

    @action
    private batchUpdate(args : any[]) {
        this.beginUpdate();

        // console.log("Batch received: " + new Date());

        for (const event of args as BufferedEvent[]) {
            this.onEvent(event.eventId, this.name, ...event.arguments);
        }

        this.updateIndices();
        this.updateRowOrder();
        this.refresh();

        // console.log("Batch processed: " + new Date());
        this.endUpdate();
    }

    protected onEvent(event : GridEvent, name : string, ...args : any[]) : void {
        if (this.name !== name) {
            return;
        }

        // const eventName = event < 0 ? CommonEvent[event] : GridEvent[event];
        // console.log(`GridEvent<${name}, ${eventName}|${event}>: ${JSON.stringify(args)}`);

        switch (event) {
            case CommonEvent.Batch as number:
                this.batchUpdate(args);
                break;
            case GridEvent.ColumnChanged:
                const [colProperty, colEvent, colArgs] = args;

                this.onColumnChanged(colProperty, colEvent, colArgs);
                break;
            case GridEvent.GroupsChanged:
                const [groupName, groupEvent, groupArgs] = args;

                this.onGroupsChanged(groupName, groupEvent, groupArgs);
                break;
            case GridEvent.RowChanged:
                const [rowIndex, rowEvent, rowArgs] = args;

                this.onRowChanged(rowIndex, rowEvent, rowArgs);
                break;
            case GridEvent.AddColumn:
                this.addColumn(args[0], false);
                break;
            case GridEvent.RemoveColumn:
                this.removeColumn(args[0], false);
                break;
            case GridEvent.SelectedColumnChanged:
                break;
            case GridEvent.ShowGroupsChanged:
                this.showGroups(args[0], true);
                break;
            case GridEvent.RowOrderChanged:
                break;
            case GridEvent.GroupByChanged:
                break;
            case GridEvent.SortRows:
                this.sort(args[0], false);
                break;
            case GridEvent.UnsortRows:
                this.unsort(false);
                break;
            case GridEvent.UnselectRows:
                this.unselectRows(-1);
                this.selectedRows = [];
                break;
            case GridEvent.SelectRows:
                break;
            case GridEvent.SelectRow:
                break;
            case GridEvent.RemoveRow: {
                const [index] = args;

                this.removeRow(index, true);
            }
                break;
            case GridEvent.SortedColumnChanged:
                this.sortedColumn = args[0];
                break;
            case GridEvent.AddRow: {
                let [index, row] = args;

                if (row.GroupName || row.GroupName === "") {
                    row = {
                        data: row
                    };
                }

                if (index !== -1) {
                    this.insertRow(row, index, true, false);
                }
                else {
                    this.addRow(row, true, false, true);
                }
            }
                break;
            case GridEvent.AddGroup:
                this.addGroup(new GroupModel(args[0], this.groups.length, [], this.onGroupEvent), true);
                break;
            case GridEvent.Clear:
                this.clear();
                break;
            case GridEvent.FilterChanged:
                this.setFilter(args[0], false);
                break;
            default:
                super.onEvent(event, name, ...args);
                break;
        }
    }
}
