import { observable, computed, action, toJS, autorun, autorunAsync } from "mobx";
import * as CssSelectorGenerator from "css-selector-generator";
import * as _ from "lodash";
import "mark.js";

import { WebEvent, WebControl, DSMessage, EventArguments } from "@dewesoft-web/ui/events";

import { GridRowModel } from "./GridRowModel";
import { GridColumnModel } from "./GridColumnModel";
import { GridGroupModel, GroupEvent } from "./GridGroupModel";

import { Column, GridColumns, GroupedRows, Row, SelectOption, GridGroups, SortDirecton } from "../types";
import { objectToArray } from "../util";

import { ControlModel } from "@dewesoft-web/ui/controls/ControlModel";

export declare class Mark {
    constructor(context : HTMLElement | string);

    mark(sv, opt?) : Mark;

    unmark(opt?) : Mark;
}
//
export declare class CssSelectorMaker {
    constructor(opts?);

    getSelector(el) : string;
}

interface Map {
    [key : string] : number;
}

interface Element {
    scrollIntoViewIfNeeded(optCenter? : boolean);
}

enum GridEvent {
    Initialized,
    Initializing,
    ColumnChanged,
    GroupsChanged,
    GroupChanged,
    RowChanged,
    SelectedColumnChanged,
    ShowGroupsChanged,
    RowOrderChanged,
    GroupByChanged,
    UnselectRows,
    SelectRows,
    SelectRow,
    RemoveRow,
    SetColumn,
    AddRow,
    Clear,
}

@DSMessage(["dsGrid", "onEvent"])
export class DSGridModel extends ControlModel<GridEvent> {

    @observable
    rows : GridRowModel[];

    @observable
    columns : GridColumns;

    @observable
    showGroups : boolean = true;

    @observable
    groupByColumn : string;

    @observable
    private sortedColumn : string;

    @observable
    private sortDirection : SortDirecton;

    @observable
    searchQuery : string;

    @observable
    selectedColumn : GridColumnModel;

    private selectedRows : GridRowModel[];

    private idToIndex : Map;

    private isSelecting : boolean;

    private rowGroups : GridGroups;

    private selectionStart : number;

    private selectionEnd : number;

    private editingRow : GridRowModel;

    private isInitialized : boolean;

    private marker : Mark;

    private markOptions : Object;

    private selectorGenerator : CssSelectorMaker;

    private selector : string;

    constructor(rows : Row[], cols : Column[], name : string, onInit? : (model : DSGridModel) => void) {
        super(WebControl.Grid, name);

        this.isInitialized = false;
        this.editingRow = undefined;
        this.selectionStart = -1;
        this.selectionEnd = -1;
        this.groupByColumn = "GroupName";
        this.rowGroups = null;
        this.searchQuery = "";
        this.marker = null;
        this.markOptions = {
            separateWordSearch: false,
            caseSensitive: false
        };

        this.idToIndex = {};

        this.selectedRows = [];
        this.selectorGenerator = new CssSelectorGenerator();

        this.onRowEvent = this.onRowEvent.bind(this);
        this.onColumnEvent = this.onColumnEvent.bind(this);
        this.onGroupEvent = this.onGroupEvent.bind(this);
        this.sortRows = this.sortRows.bind(this);
        this.setElement = this.setElement.bind(this);
        this.startSelect = this.startSelect.bind(this);
        this.endSelect = this.endSelect.bind(this);
        this.onTrySelect = this.onTrySelect.bind(this);
        this.onRowSelect = this.onRowSelect.bind(this);
        this.editCell = this.editCell.bind(this);
        this.setSelectedColumnData = this.setSelectedColumnData.bind(this);
        this.selectWholeColumn = this.selectWholeColumn.bind(this);

        if (cols) {
            this.columns = {};

            const colNum = cols.length;
            for (let i = 0; i < colNum; i++) {
                this.columns[cols[i].Property] = new GridColumnModel(cols[i], i, this.onColumnEvent);
            }
        }
        else {
            this.columns = {};
        }

        if (rows) {
            this.rows = rows.map((row : Row) => {
                return new GridRowModel(row, this.columnProperties, this.onRowEvent);
            });

            this.sortRows();
        }
        else {
            this.rows = [];
        }

        this.triggerEvent(GridEvent.Initializing, {
            rows: toJS(this.rows),
            cols: objectToArray(toJS(this.columns))
        });

        autorun(() => {
            this.rowGroups = {} as any;

            for (const group of this.groups) {
                this.rowGroups[group.name] = group;
            }

            this.isInitialized = true;

            if (onInit) {
                onInit(this);
            }

            this.triggerEvent(GridEvent.GroupsChanged, {
                event: GroupEvent.Initialize,
                groups: this.rowGroups
            });
        });

        autorunAsync(() => {
            const query = this.searchQuery;

            if (!this.marker) {
                return;
            }

            if (query) {
                for (const row of this.rows) {
                    row.hideNotMatching(this.searchQuery);
                }

                this.marker.unmark();
                this.marker.mark(this.searchQuery, this.markOptions);
            }
            else {
                this.marker.unmark();

                for (const row of this.rows) {
                    row.forceVisible();
                }
            }
        });
    }

    @computed
    get searchIcon() {
        if (this.searchQuery.length === 0) {
            return "search";
        }
        else {
            return "highlight_off";
        }
    }

    @computed
    get sortedColumns() : GridColumnModel[] {
        const colNames = Object.keys(this.columns);
        const numColumns = colNames.length;
        const columns : GridColumnModel[] = new Array(numColumns);

        for (let i = 0; i < colNames.length; i++) {
            columns[i] = this.columns[colNames[i]];
        }

        columns.sort(function(lhs, rhs) {
            return lhs.order - rhs.order;
        });

        return columns;
    }

    @computed
    get columnProperties() : string[] {
        return Object.keys(this.columns);
    }

    @computed
    get rowsByGroup() : GroupedRows {
        const groupedRows = _.groupBy(this.rows, "data." + this.groupByColumn + ".value");

        if (this.sortDirection === SortDirecton.None || !this.sortedColumn) {
            return groupedRows;
        }
        else if (this.sortDirection === SortDirecton.Ascending) {
            // noinspection TsLint
            for (const group in groupedRows) {
                groupedRows[group] = _.sortBy(groupedRows[group], (row : GridRowModel) => {
                    return row.getValue(this.sortedColumn);
                });
            }
        }
        else if (this.sortDirection === SortDirecton.Descending) {
            // noinspection TsLint
            for (const g in groupedRows) {
                groupedRows[g] = _.sortBy(groupedRows[g], (row : GridRowModel) => {
                    return row.getValue(this.sortedColumn);
                }).reverse();
            }
        }

        return groupedRows;
    }

    setElement(el) {
        this.selector = this.selectorGenerator.getSelector(el) + " tbody.data";

        this.marker = new Mark(this.selector);
    }

    @action
    sortColumn(column : string) {
        if (this.sortedColumn !== column) {
            this.sortedColumn = column;
            this.sortDirection = SortDirecton.Ascending;
            return;
        }

        if (this.sortDirection === SortDirecton.Descending) {
            this.sortDirection = SortDirecton.Ascending;
        }
        else if (this.sortDirection === SortDirecton.Ascending) {
            this.sortDirection = SortDirecton.Descending;
        }
        else {
            // console.log("Should never come here");
        }
    }

    unsortColumns() {
        this.sortedColumn = null;
        this.sortDirection = SortDirecton.None;
    }

    private get groupNames() {
        return Object.keys(this.rowsByGroup);
    }

    @computed
    get groups() : GridGroupModel[] {
        return this.groupNames.map((group) => {
            return new GridGroupModel(group, this.onGroupEvent);
        });
    }

    @action
    private sortRows() {
        // console.log("Sorting rows ...");

        const groupedRows = this.rowsByGroup;

        const groups = Object.keys(groupedRows);
        const sortedRows = new Array(this.rows.length);
        const keysToIndex = new Array(this.rows.length);

        let index = 0;

        for (const group of groups) {
            const groupRows = groupedRows[group];
            const groupRowLen = groupRows.length;

            for (let i = 0; i < groupRowLen; i++) {
                keysToIndex[index] = groupRows[i].uniqueId;
                groupRows[i].setIndex(index);

                sortedRows[index++] = groupRows[i];
            }
        }

        this.rows = sortedRows;

        this.triggerEvent(GridEvent.RowOrderChanged, {
            keys: keysToIndex
        });
    }

    @action
    selectWholeColumn(col : string) {
        // this.selectedRows = [];
        // this.selectedRows = this.rows;
        //
        // if (this.selectedColumn) {
        //     this.selectedColumn.setSelected(false);
        // }
        //
        // this.selectedColumn = this.columns[col];
        // this.selectedColumn.setSelected(true);
        //
        // for (const row of this.selectedRows) {
        //     row.select();
        // }

        // if (!this.selectedColumn) {
        //     return;
        // }
        //
        // const columnCells = document.querySelectorAll(`td[data-dsgrid-col='${this.selectedColumn.property}']`);
        //
        // for (let i = 0; i < columnCells.length; i++) {
        //     columnCells.item(i).classList.add("selected_GridCell1QiS1");
        // }

    }

    @action
    startSelect(row : GridRowModel, col : string) {

        for (const selectedRow of this.selectedRows) {
            selectedRow.deselect();
        }

        this.selectionStart = row.getIndex();
        this.selectedRows = [row];
        this.isSelecting = true;

        row.first = true;
        row.last = true;

        if (row) {
            if (this.selectedColumn) {
                this.selectedColumn.setSelected(false);
            }

            this.selectedColumn = this.columns[col];
            this.selectedColumn.setSelected(true);

            row.select();
        }
    }

    @action
    onRowSelect(row : GridRowModel, col : string, selectToRow? : boolean) {
        if (col !== this.selectedColumn.property || this.columns[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) {
            this.selectRows(this.selectionStart, row.getIndex());

            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
    onTrySelect(row : GridRowModel) {
        if (!this.isSelecting || this.selectedColumn.readOnly) {
            return;
        }

        this.selectionEnd = row.getIndex();

        if (this.selectedRows.length) {
            this.unselectRows(this.selectedRows[0]);
        }
        this.selectRows(this.selectionStart, this.selectionEnd);

        if (this.selectedRows.length) {
            this.selectedRows[0].first = true;
            this.selectedRows[this.selectedRows.length - 1].last = true;
        }
    }

    @action
    selectRows(startIndex : number, endIndex : number) {
        this.unselectAllRows();
        this.selectedRows = [];

        if (endIndex < startIndex) {
            let temp = endIndex;

            endIndex = startIndex;
            startIndex = temp;
        }

        const numRows = this.rows.length;
        for (let index = 0; index < numRows; index++) {
            const select = (index >= startIndex) && (index <= endIndex);

            this.rows[index].setSelected(select);

            if (select) {
                this.selectedRows.push(this.rows[index]);
            }
        }
    }

    private scrollElementIntoView(selector : string, alignTop : boolean = false) {
        const element = document.querySelector(selector) as HTMLElement;

        if (!element) {
            return;
        }

        if (element.scrollIntoView) {
            const gridWrapperEl = document.querySelector(this.selector).parentElement.parentElement;

            const wrapperTop = gridWrapperEl.scrollTop;
            const wrapperHeight = gridWrapperEl.clientHeight - element.clientHeight;

            const elementTop = element.offsetTop;
            const elementBottom = elementTop + element.clientHeight;

            if (elementBottom > wrapperTop + wrapperHeight || elementTop < wrapperTop) {
                element.scrollIntoView(alignTop);
            }
        }
    }

    @action
    selectFirstRow() {
        this.unselectAllRows();

        this.selectionStart = 0;
        this.selectionEnd = 0;

        const firstRow = this.rows[0];
        this.selectedRows = [firstRow];
        firstRow.select();

        this.scrollElementIntoView(this.selector + " tr:first-of-type");
    }

    @action
    selectLastRow() {
        this.unselectAllRows();

        this.selectionStart = this.rows.length - 1;
        this.selectionEnd = this.rows.length - 1;

        const lastRow = this.rows[this.rows.length - 1];

        if (lastRow) {
            this.selectedRows = [lastRow];
            lastRow.select();
        }

        this.scrollElementIntoView(this.selector + " tr:last-of-type");
    }

    @action
    selectNextRow() {
        const length = this.rows.length - 1;
        if (this.selectionStart >= length) {
            return;
        }

        this.unselectAllRows();

        const nextRow = this.rows[++this.selectionStart];
        this.selectionEnd = this.selectionStart;

        if (nextRow) {
            this.selectedRows = [nextRow];

            nextRow.select();
        }

        this.scrollElementIntoView(`${this.selector} tr:nth-of-type(${this.selectionStart})`, true);
    }

    @action
    selectPreviousRow() {
        if (this.selectionStart < 0) {
            this.selectionStart = 0;
            return;
        }

        if (this.selectionStart === 0) {
            return;
        }

        this.unselectAllRows();

        this.rows[this.selectionStart].deselect();

        const prevRow = this.rows[--this.selectionStart];
        this.selectionEnd = this.selectionStart;

        this.selectedRows = [prevRow];
        prevRow.select();

        if (this.selectionStart === 0) {
            this.selectFirstRow();
        }
        else {
            this.scrollElementIntoView(`${this.selector} tr:nth-of-type(${this.selectionStart})`);
        }
    }

    @action
    unselectRows(except : GridRowModel) {
        const length = this.selectedRows.length;
        for (let i = 0; i < length; i++) {
            if (this.selectedRows[i] !== except) {
                this.selectedRows[i].deselect();
            }
        }
    }

    @action
    unselectAllRows() {
        const length = this.rows.length;
        for (let i = 0; i < length; i++) {
            this.rows[i].deselect();
        }
    }

    @action
    addRow(row : Row) {
        this.rows.push(new GridRowModel(row, this.columnProperties, this.onRowEvent));

        this.triggerEvent(GridEvent.AddRow, {
            row: toJS(this.rows[this.rows.length - 1])
        });
    }

    @action
    removeRows(startIndex : number, count : number = 1) {
        // console.log(`removingRow, start: ${startIndex}, count: ${count}`);

        const removed = this.rows.splice(startIndex, count);
        this.sortRows();

        this.triggerEvent(GridEvent.RemoveRow, {
            removedIds: removed.map(function(removedRow : GridRowModel) {
                return {
                    uniqueId: removedRow.uniqueId,
                    index: removedRow.getIndex()
                };
            })
        });
    }

    @action
    setGroupBy(col : string, noEvent? : boolean) {
        this.groupByColumn = col;

        this.sortRows();

        if (noEvent) {
            return;
        }

        this.triggerEvent(GridEvent.GroupByChanged, {
            column: col
        });
    }

    @action
    setShowGroups(show : boolean) {
        if (this.showGroups === show) {
            return;
        }

        this.showGroups = show;

        this.triggerEvent(GridEvent.ShowGroupsChanged, {
            show: show
        });
    }

    @action
    endSelect() {
        this.isSelecting = false;
    }

    @action
    setSelectedColumnData(value : any) {
        for (const row of this.selectedRows) {
            row.setValue(this.selectedColumn.property, value);
        }
    }

    @action
    editCell(row : GridRowModel, col : string) {
        if (!row.data[col]) {
            return;
        }

        if (row.data[col].type && row.data[col].type.readOnly) {
            return;
        }

        if (this.editingRow) {
            this.editingRow.setEditing(false, this.selectedColumn.property);
        }

        // console.log(`Store: edit ${col}`);

        this.editingRow = row;
        row.setEditing(true, col);
    }

    getColumnDescriptors() : SelectOption[] {
        const properties = this.columnProperties;

        const size = properties.length;
        const descriptions : SelectOption[] = new Array(size);

        for (let i = 0; i < size; i++) {
            descriptions[i] = {
                value: properties[i],
                description: this.columns[properties[i]].title
            };
        }

        return descriptions;
    }

    onInitialized() {

    }

    protected triggerEvent(event : GridEvent, args : EventArguments) {
        if (event !== GridEvent.Initializing && !this.isInitialized) {
            return;
        }

        super.triggerEvent(event, args);
    }

    private onColumnEvent(column : GridColumnModel, event : number, args : EventArguments) {
        this.triggerEvent(GridEvent.ColumnChanged, {
            columnEvent: event,
            column: column.property,
            arguments: args
        });
    }

    private onRowEvent(row : GridRowModel, event : number, args : EventArguments) {
        this.triggerEvent(GridEvent.RowChanged, {
            rowEvent: event,
            uniqueId: row.uniqueId,
            arguments: args
        });
    }

    private onGroupEvent(group : GridGroupModel, event : number, args : EventArguments) {
        this.triggerEvent(GridEvent.GroupsChanged, {
            groupEvent: event,
            group: group.name,
            arguments: args
        });
    }

    /*
     * C++ event handlers
     */

    protected onEvent(event : GridEvent, name : string, ...args : any[]) {
        if (name !== this.name) {
            return;
        }

        switch (event) {
            case GridEvent.ColumnChanged:
                const [colProp, colEvent, colArgs] = args;
                const col = this.columns[colProp];

                if (col) {
                    col.onEvent(colEvent, ...colArgs);
                }

                break;
            case GridEvent.RowChanged:
                const [rowIdx, rowEvent, rowArgs] = args;

                (this.rows[rowIdx].onEvent as any)(rowEvent, ...rowArgs);
                break;
            case GridEvent.GroupsChanged:
                const [groupName, groupEvent, groupArgs] = args;
                const [groupEventName, groupArguments] = groupArgs;

                this.rowGroups[groupName].onEvent(groupEvent, ...groupArguments);
                break;
            case GridEvent.GroupByChanged:
                this.setGroupBy(args[0], true);
                break;
            case GridEvent.SetColumn:
                this.setColumn(args[0], args[1]);
                break;
            case GridEvent.AddRow:
                this.onAddRow(args[0], args[1], args[2]);
                break;
            case GridEvent.RemoveRow:
                this.removeRows(args[0]);
                break;
            case GridEvent.SelectRow:
                // this.selectSingleRowByIndex(args[0]);
                break;
            case GridEvent.SelectRows:
                this.selectRows(args[0], args[1]);
                break;
            case GridEvent.UnselectRows:
                this.unselectAllRows();
                break;
            case GridEvent.Clear:
                this.rows = [];
                break;
            default:
                break;
        }
    }

    private onAddRow(rowIndex : number, addedRow : AddRow, uniqueId : string) {
        this.rows.splice(rowIndex, 0, new GridRowModel(addedRow, this.columnProperties, uniqueId, this.onRowEvent));
    }

    private setColumn(property : string, column : any) {

    }
}

interface AddRow {
    rowIndex : number;
    data : any;
}
