import '../css/grid.scss';
import { global, isPositive, isMobile, throttle, truncate } from "../../utility";
import { r } from "../../utility/lgres";
import { nullOrEmpty } from "../../utility/strings";
import { createElement } from "../../functions";
import { createIcon } from "../icon";
import { createCheckbox } from "../checkbox";
import { setTooltip } from "../tooltip";
import { GridColumn, GridInputColumn, GridTextColumn, GridDropdownColumn, GridCheckboxColumn, GridIconColumn } from "./column";

const ColumnChangedType = {
    Reorder: 'reorder',
    Resize: 'resize',
    Sort: 'sort'
};
const RefreshInterval = isMobile() ? 32 : 0;
const HoverInternal = 200;
const RedumCount = 4;
const MiniDragOffset = 4;
const MiniColumnWidth = 50;
const FilterPanelWidth = 200;

function getClientX(e) {
    if (e == null) {
        return null;
    }
    const cx = e.touches && e.touches[0]?.clientX;
    return cx ?? e.clientX;
}

function getOffsetLeftFromWindow(element) {
    let left = 0;
    while (element != null) {
        left += element.offsetLeft;
        element = element.offsetParent;
    }
    return left;
}

function indexOfParent(target) {
    // return [...target.parentElement.children].indexOf(target);
    return Array.prototype.indexOf.call(target.parentElement.children, target);
}

const ColumnTypes = {
    0: GridColumn,
    1: GridInputColumn,
    2: GridDropdownColumn,
    3: GridCheckboxColumn,
    4: GridIconColumn,
    5: GridTextColumn
};

class Grid {
    #source;
    #currentSource;
    #parent;
    #el;
    #refs;
    #rendering;
    #selectedColumnIndex = -1;
    #selectedIndexes;
    #startIndex = 0;
    #needResize;
    #containerHeight;
    #bodyClientWidth;
    #rowCount = -1;
    #scrollTop;
    #scrollLeft;
    #colTypes = {};
    #colAttrs = {};
    #vtable = [];

    columns = [];
    langs = {
        all: r('allItem', '( All )'),
        ok: r('ok', 'OK'),
        reset: r('reset', 'Reset')
    };
    virtualCount = 100;
    rowHeight = 36;
    lineHeight = 24;
    extraRows = 0;
    filterRowHeight = 30;
    height;
    readonly;
    multiSelect = false;
    fullrowClick = true;
    allowHtml = false;
    holderDisabled = false;
    headerVisible = true;
    window = global;
    sortIndex = -1;
    sortDirection = 1;

    willSelect;
    selectedRowChanged;
    cellDblClicked;
    cellClicked;
    rowDblClicked;
    columnChanged;

    static ColumnTypes = {
        Common: 0,
        Input: 1,
        Dropdown: 2,
        Checkbox: 3,
        Icon: 4,
        Text: 5,
        isCheckbox(type) { return type === 3 }
    };

    static GridColumn = GridColumn;

    constructor(container) {
        this.#parent = container;
    }

    get element() { return this.#el }

    get source() { return this.#source?.map(s => s.values) }
    set source(list) {
        if (this.#el == null) {
            throw new Error('grid has not been initialized.')
        }
        if (!Array.isArray(list)) {
            throw new Error('source is not an Array.')
        }
        list = list.map(i => { return { values: i } });
        this.#source = list;
        this.#refreshSource(list);
    }

    #refreshSource(list) {
        list ??= this.#source;
        if (this.#colAttrs.__filtered === true) {
            this.#currentSource = list.filter(it => {
                for (let col of this.columns) {
                    if (Array.isArray(col.filterValues)) {
                        const v = this.#getItemValue(it.values, col.key, col.filter);
                        if (col.filterValues.indexOf(v) < 0) {
                            return false;
                        }
                    }
                }
                return true;
            });
        } else {
            this.#currentSource = list;
        }
        this.#selectedColumnIndex = -1;
        this.#selectedIndexes = [];
        this.#startIndex = 0;
        this.#scrollTop = 0;
        this.#scrollLeft = 0;
        this.#rowCount = -1;

        if (this.sortIndex >= 0) {
            this.sortColumn();
        }
        this.resize();
    }

    get virtual() { return this.#currentSource?.length > this.virtualCount }

    get sortKey() {
        if (this.columns == null) {
            return null;
        }
        return this.columns[this.sortIndex]?.key;
    }

    get selectedIndexes() { return this.#selectedIndexes }
    set selectedIndexes(indexes) {
        const startIndex = this.#startIndex;
        this.#selectedIndexes.splice(0, this.#selectedIndexes.length, ...indexes);
        if (this.readonly !== true) {
            this.refresh();
        } else {
            [...this.#refs.bodyContent.children].forEach((row, i) => {
                if (indexes.indexOf(startIndex + i) >= 0) {
                    row.classList.add('selected');
                } else if (row.classList.contains('selected')) {
                    row.classList.remove('selected');
                }
            });
        }
        if (typeof this.selectedRowChanged === 'function') {
            this.selectedRowChanged();
        }
    }

    get selectedIndex() { return (this.#selectedIndexes && this.#selectedIndexes[0]) ?? -1 }

    get loading() { return this.#refs.loading?.style?.visibility === 'visible' }
    set loading(flag) {
        if (this.#refs.loading == null) {
            return;
        }
        if (flag === false) {
            this.#refs.loading.style.visibility = 'hidden';
            this.#refs.loading.style.opacity = 0;
        } else {
            this.#refs.loading.style.visibility = 'visible';
            this.#refs.loading.style.opacity = 1;
        }
    }

    get scrollTop() { return this.#refs.body?.scrollTop; }
    set scrollTop(top) {
        if (this.#refs.body == null) {
            return;
        }
        this.#refs.body.scrollTop = top;
        this.reload();
    }

    init(container = this.#parent) {
        this.#el = null;
        this.#refs = {};
        this.#rendering = true;
        if (!(container instanceof HTMLElement)) {
            throw new Error('no specified parent.');
        }
        this.#parent = container;
        const grid = createElement('div', 'ui-grid');
        grid.setAttribute('tabindex', 0);
        grid.addEventListener('keydown', e => {
            let index = this.selectedIndex;
            let flag = false;
            if (e.key === 'ArrowUp') {
                // up
                if (index > 0) {
                    flag = true;
                    index -= 1;
                }
            } else if (e.key === 'ArrowDown') {
                // down
                const count = this.#currentSource?.length ?? 0;
                if (index < count - 1) {
                    flag = true;
                    index += 1;
                }
            }
            if (flag) {
                this.#selectedIndexes = [index];
                this.scrollToIndex(index);
                this.refresh();
                if (typeof this.selectedRowChanged === 'function') {
                    this.selectedRowChanged(index);
                }
                e.stopPropagation();
            }
        });
        container.replaceChildren(grid);
        const sizer = createElement('span', 'ui-grid-sizer');
        grid.appendChild(sizer);
        this.#refs.sizer = sizer;

        // header & body
        const header = this.#createHeader();
        grid.appendChild(header);
        const body = this.#createBody();
        grid.appendChild(body);

        // loading
        const loading = createElement('div', 'ui-grid-loading',
            createElement('div', null, createIcon('fa-regular', 'spinner-third'))
        );
        this.#refs.loading = loading;
        grid.appendChild(loading);
        this.#el = grid;

        this.#rendering = false;
        if (this.#source != null && this.sortIndex >= 0) {
            this.sortColumn();
        }
    }

    scrollToIndex(index) {
        const top = this.#scrollToTop(index * (this.rowHeight + 1), true);
        this.#refs.body.scrollTop = top;
    }

    resize(force) {
        if (this.#rendering || this.#el == null) {
            return;
        }
        const body = this.#refs.body;
        // let height = this.#refs.header.offsetHeight + 2;
        // let top = body.offsetTop;
        // if (top !== height) {
        //     body.style.top = `${height}px`;
        //     top = height;
        // }
        const top = this.headerVisible === false ? 0 : this.#refs.header.offsetHeight;

        let height = this.height;
        if (height === 0) {
            height = this.#containerHeight;
        } else if (isNaN(height) || height < 0) {
            height = this.#el.offsetHeight - top;
        }
        const count = truncate((height - 1) / (this.rowHeight + 1)) + (RedumCount * 2) + 1;
        if (force || count !== this.#rowCount) {
            this.#rowCount = count;
            this.reload();
        }
        this.#bodyClientWidth = body.clientWidth;
    }

    reload() {
        let length = this.#currentSource.length;
        if (this.extraRows > 0) {
            length += this.extraRows;
        }
        this.#containerHeight = length * (this.rowHeight + 1);
        this.#refs.body.scrollTop = 0;
        this.#refs.body.scrollLeft = 0;
        this.#refs.bodyContent.style.top = '0px';
        this.#refs.bodyContainer.style.height = `${this.#containerHeight}px`;
        this.#adjustRows(this.#refs.bodyContent);
        this.refresh();
    }

    refresh() {
        if (this.#refs.bodyContent == null) {
            throw new Error('body has not been created.');
        }
        const rows = this.#refs.bodyContent.children;
        const widths = {};
        this.#fillRows(rows, this.columns, widths);
        if (this.#needResize && widths.flag) {
            this.#needResize = false;
            this.columns.forEach((col, i) => {
                if (!this.#get(col.key, 'autoResize')) {
                    return;
                }
                let width = widths[i];
                if (width < col.width) {
                    width = col.width;
                }
                if (width > 0) {
                    this.#changeColumnWidth(i, width);
                }
            });
        }
    }

    resetChange() {
        if (this.#source == null) {
            return;
        }
        for (let row of this.#source) {
            delete row.__changed;
        }
    }

    sortColumn(reload) {
        const index = this.sortIndex;
        const col = this.columns[index];
        if (col == null) {
            return;
        }
        const direction = this.sortDirection;
        [...this.#refs.header.children].forEach((th, i) => {
            const arrow = th.querySelector('.arrow');
            if (arrow == null) {
                return;
            }
            if (i === index) {
                arrow.className = `arrow ${(direction !== 1 ? 'desc' : 'asc')}`;
            } else if (arrow.className !== 'arrow') {
                arrow.className = 'arrow';
            }
        });
        let comparer;
        if (typeof col.sortFilter !== 'function') {
            const direction = this.sortDirection;
            if (isNaN(direction)) {
                direction = 1;
            }
            comparer = (a, b) => {
                a = this.#getItemValue(a.values, col.key, col.filter);
                b = this.#getItemValue(b.values, col.key, col.filter);
                if (a == null && typeof b === 'number') {
                    a = 0;
                } else if (typeof a === 'number' && b == null) {
                    b = 0;
                } else if (a != null && b == null) {
                    return direction;
                } else if (typeof a === 'string' && typeof b === 'string') {
                    a = a.toLowerCase();
                    b = b.toLowerCase();
                }
                return a === b ? 0 : (a > b ? 1 : -1) * direction;
            };
        } else {
            comparer = (a, b) => col.sortFilter(a.values, b.values) * direction;
        }
        this.#source.sort(comparer);
        if (this.#colAttrs.__filtered === true) {
            this.#currentSource.sort(comparer);
        }
        if (this.#rowCount < 0) {
            return;
        }
        if (reload) {
            this.reload();
        } else {
            this.refresh();
        }
    }

    #createHeader() {
        const thead = createElement('table', 'ui-grid-header');
        if (this.headerVisible === false) {
            thead.style.display = 'none';
        }
        const header = createElement('tr');
        thead.appendChild(header);
        const sizer = this.#refs.sizer;
        for (let col of this.columns) {
            if (col.visible === false) {
                const hidden = createElement('th');
                hidden.style.display = 'none';
                if (col.sortable !== false) {
                    hidden.dataset.key = col.key;
                    hidden.addEventListener('click', e => this.#onHeaderClicked(e, col, true));
                }
                header.appendChild(hidden);
                continue;
            }
            // style
            const isCheckbox = Grid.ColumnTypes.isCheckbox(col.type);
            if (col.width > 0) {
                // col.autoResize = false;
            } else {
                this.#set(col.key, 'autoResize', true);
                this.#needResize = true;
                sizer.innerText = col.caption ?? '';
                let width = sizer.offsetWidth + 22;
                if (!this.readonly && col.enabled !== false && col.allcheck && isCheckbox) {
                    width += 32;
                }
                if (col.allowFilter === true) {
                    width += 14;
                }
                if (width < MiniColumnWidth) {
                    width = MiniColumnWidth;
                }
                col.width = width;
            }
            col.align ??= isCheckbox ? 'center' : 'left';
            if (col.sortable !== false) {
                col.sortable = true;
            }
            const w = `${col.width}px`;
            const style = {
                'width': w,
                'max-width': w,
                'min-width': w,
                'text-align': col.align
            };
            this.#set(col.key, 'style', style);
            // element
            const th = createElement('th', 'column');
            th.dataset.key = col.key;
            for (let css of Object.entries(style)) {
                th.style.setProperty(css[0], css[1]);
            }
            if (col.sortable) {
                th.style.cursor = 'pointer';
                th.addEventListener('click', e => this.#onHeaderClicked(e, col));
            }
            if (col.orderable !== false) {
                col.orderable = true;
                th.addEventListener('mousedown', e => this.#onDragStart(e, col));
            }
            const wrapper = createElement('div');
            th.appendChild(wrapper);
            if (!this.readonly && col.enabled !== false && col.allcheck && isCheckbox) {
                const check = createCheckbox({
                    onchange: e => this.#onColumnAllChecked(col, e.target.checked)
                });
                wrapper.appendChild(check);
            }
            const caption = createElement('span');
            if (col.textStyle != null) {
                for (let css of Object.entries(col.textStyle)) {
                    caption.style.setProperty(css[0], css[1]);
                }
            }
            caption.innerText = col.caption ?? '';
            wrapper.appendChild(caption);
            // order arrow
            if (col.sortable) {
                th.appendChild(createElement('layer', 'arrow'));
            }
            // filter
            if (col.allowFilter === true) {
                const filter = createElement('layer', 'filter');
                filter.appendChild(createIcon('fa-solid', 'filter'));
                filter.addEventListener('mousedown', e => this.#onFilter(e, col));
                th.classList.add('header-filter');
                th.appendChild(filter);
            }
            // resize spliter
            if (col.resizable !== false) {
                const spliter = createElement('layer', 'spliter');
                spliter.addEventListener('mousedown', e => this.#onResizeStart(e, col));
                spliter.addEventListener('dblclick', e => this.#onAutoResize(e, col));
                th.appendChild(spliter);
            }
            // tooltip
            // !nullOrEmpty(col.tooltip) && setTooltip(th, col.tooltip);
            header.appendChild(th);
        }
        const dragger = createElement('div', 'dragger');
        const draggerCursor = createElement('layer', 'dragger-cursor');
        header.appendChild(createElement('th', null, dragger, draggerCursor));

        sizer.replaceChildren();
        this.#refs.header = header;
        this.#refs.dragger = dragger;
        this.#refs.draggerCursor = draggerCursor;
        return thead;
    }

    #createBody() {
        const body = createElement('div', 'ui-grid-body');
        body.addEventListener('scroll', e => throttle(this.#onScroll, RefreshInterval, this, e), { passive: true });
        const cols = this.columns;
        let width = 1;
        for (let col of cols) {
            if (col.visible !== false && !isNaN(col.width)) {
                width += col.width + 1;
            }
        }
        // body container
        const bodyContainer = createElement('div');
        bodyContainer.style.position = 'relative';
        bodyContainer.style.minWidth = '100%';
        bodyContainer.style.minHeight = '1px';
        if (width > 0) {
            bodyContainer.style.width = `${width}px`;
        }
        body.appendChild(bodyContainer);
        // body content
        const bodyContent = createElement('table', 'ui-grid-body-content');
        bodyContent.addEventListener('mousedown', e => {
            let [parent, target] = this.#getRowTarget(e.target);
            const rowIndex = indexOfParent(parent);
            let colIndex = indexOfParent(target);
            if (colIndex >= this.columns.length) {
                colIndex = -1;
            }
            this.#onRowClicked(e, rowIndex, colIndex);
        });
        bodyContent.addEventListener('dblclick', e => this.#onRowDblClicked(e));
        bodyContainer.appendChild(bodyContent);
        // this.#adjustRows();
        // events
        if (!this.holderDisabled) {
            const holder = createElement('div', 'ui-grid-hover-holder');
            holder.addEventListener('mousedown', e => {
                const holder = e.currentTarget;
                const row = Number(holder.dataset.row);
                const col = Number(holder.dataset.col);
                if (holder.classList.contains('active')) {
                    holder.classList.remove('active');
                }
                return this.#onRowClicked(e, row + this.#startIndex, col);
            });
            holder.addEventListener('dblclick', e => this.#onRowDblClicked(e));
            bodyContainer.appendChild(holder);
            body.addEventListener('mousemove', e => throttle(this.#onBodyMouseMove, HoverInternal, this, e, holder), { passive: true });
        }
        this.#refs.body = body;
        this.#refs.bodyContainer = bodyContainer;
        this.#refs.bodyContent = bodyContent;

        // this.refresh();
        return body;
    }

    #adjustRows() {
        let count = this.#rowCount;
        if (isNaN(count) || count < 0 || !this.virtual) {
            count = this.#currentSource.length;
        }
        const cols = this.columns;
        const content = this.#refs.bodyContent;
        const exists = content.children.length;
        count -= exists;
        if (count > 0) {
            for (let i = 0; i < count; i += 1) {
                const row = createElement('tr', 'ui-grid-row');
                // row.addEventListener('mousedown', e => this.#onRowClicked(e, exists + i));
                // row.addEventListener('dblclick', e => this.#onRowDblClicked(e));
                cols.forEach((col, j) => {
                    const cell = createElement('td');
                    if (col.visible !== false) {
                        cell.dataset.row = String(exists + i);
                        cell.dataset.col = String(j);
                        const style = this.#get(col.key, 'style');
                        if (style != null) {
                            for (let css of Object.entries(style)) {
                                cell.style.setProperty(css[0], css[1]);
                            }
                        }
                        if (col.css != null) {
                            for (let css of Object.entries(col.css)) {
                                cell.style.setProperty(css[0], css[1]);
                            }
                        }
                        if (Grid.ColumnTypes.isCheckbox(col.type)) {
                            cell.appendChild(GridCheckboxColumn.createEdit(e => this.#onRowChanged(e, exists + i, col, e.target.checked, cell)));
                            // this.#colTypes[col.key] = GridCheckboxColumn;
                        } else {
                            let type = this.#colTypes[col.key];
                            if (type == null) {
                                if (isNaN(col.type)) {
                                    if (this.allowHtml && col.type != null) {
                                        type = col.type;
                                    }
                                } else {
                                    type = ColumnTypes[col.type];
                                }
                                type ??= GridColumn;
                                this.#colTypes[col.key] = type;
                            }
                            cell.appendChild(type.create(col));
                        }
                    }
                    row.appendChild(cell);
                });
                row.appendChild(createElement('td'));
                content.appendChild(row);
            }
        } else if (count < 0) {
            for (let i = -1; i >= count; i -= 1) {
                // content.removeChild(content.children[exists + i]);
                content.children[exists + i].remove();
            }
        }
    }

    #fillRows(rows, cols, widths) {
        const startIndex = this.#startIndex;
        const selectedIndexes = this.#selectedIndexes;
        [...rows].forEach((row, i) => {
            const vals = this.#currentSource[startIndex + i];
            if (vals == null) {
                return;
            }
            if (!isPositive(row.children.length)) {
                return;
            }
            const item = vals.values;
            const selected = selectedIndexes.indexOf(startIndex + i) >= 0;
            if (selected) {
                row.classList.add('selected');
            } else if (row.classList.contains('selected')) {
                row.classList.remove('selected');
            }
            // data
            const selectChanged = vals.__selected ^ selected;
            if (selected) {
                vals.__selected = true;
            } else {
                delete vals.__selected;
            }
            cols.forEach((col, j) => {
                if (col.visible === false) {
                    return;
                }
                let val;
                if (col.text != null) {
                    val = col.text;
                } else if (typeof col.filter === 'function') {
                    val = col.filter(item);
                } else {
                    val = item[col.key];
                    if (val?.displayValue != null) {
                        val = val.displayValue;
                    }
                }
                val ??= '';
                // fill
                const cell = row.children[j];
                if (typeof col.bgFilter === 'function') {
                    const bgColor = col.bgFilter(item);
                    cell.style.backgroundColor = bgColor ?? '';
                }
                const isCheckbox = Grid.ColumnTypes.isCheckbox(col.type);
                const type = isCheckbox ? GridCheckboxColumn : this.#colTypes[col.key] ?? GridColumn;
                let element;
                if (!isCheckbox && selectChanged && typeof type.createEdit === 'function') {
                    if (vals.__editing?.[col.key] && type.editing) {
                        val = type.getValue({ target: cell.children[0] });
                        this.#onRowChanged(null, startIndex + i, col, val, cell, true);
                    }
                    element = selected ?
                        type.createEdit(e => this.#onRowChanged(e, startIndex + i, col, type.getValue(e), cell), col, this.#refs.bodyContent, vals) :
                        type.create(col);
                    cell.replaceChildren(element);
                } else {
                    element = cell.children[0];
                }
                let enabled;
                if (this.readonly) {
                    enabled = false;
                } else {
                    enabled = col.enabled;
                    if (typeof enabled === 'function') {
                        enabled = enabled.call(col, item);
                    } else if (typeof enabled === 'string') {
                        enabled = item[enabled];
                    }
                }
                type.setValue(element, val, item, col, this);
                let tip = col.tooltip;
                if (typeof tip === 'function') {
                    tip = tip.call(col, item);
                }
                if (nullOrEmpty(tip)) {
                    element.querySelector('.ui-tooltip-wrapper')?.remove();
                } else {
                    setTooltip(element, tip, false, this.element);
                }
                if (typeof type.setEnabled === 'function') {
                    type.setEnabled(element, enabled);
                }
                // auto resize
                if (this.#needResize && this.#get(col.key, 'autoResize')) {
                    const width = element.scrollWidth + 12;
                    if (width > 0 && widths != null && (isNaN(widths[j]) || widths[j] < width)) {
                        widths[j] = width;
                        widths.flag = true;
                    }
                }
                if (typeof col.styleFilter === 'function') {
                    const style = col.styleFilter(item);
                    if (style != null) {
                        type.setStyle(element, style);
                    }
                }
                if (col.events != null) {
                    for (let ev of Object.entries(col.events)) {
                        element[ev[0]] = ev[1].bind(item);
                    }
                }
                if (col.attrs != null) {
                    let attrs = col.attrs;
                    if (typeof attrs === 'function') {
                        attrs = attrs(item);
                    }
                    for (let attr of Object.entries(attrs)) {
                        element.setAttribute(attr[0], attr[1]);
                    }
                }
            });
            if (vals.__editing != null) {
                delete vals.__editing;
            }
        });
    }

    #changeColumnWidth(index, width) {
        const col = this.columns[index];
        // const oldwidth = col.width;
        const w = `${width}px`;
        col.width = width;
        const style = this.#get(col.key, 'style');
        style.width = w;
        style['max-width'] = w;
        style['min-width'] = w;
        let element = this.#refs.header.children[index];
        element.style.width = w;
        element.style.maxWidth = w;
        element.style.minWidth = w;
        const body = this.#refs.bodyContent;
        for (let row of body.children) {
            element = row.children[index];
            if (element != null) {
                element.style.width = w;
                element.style.maxWidth = w;
                element.style.minWidth = w;
            }
        }
        // } else {
        //     width = this.#refs.bodyContainer.offsetWidth - oldwidth + width;
        //     this.#refs.bodyContainer.style.width = `${width}px`;
        // }
    }

    #changingColumnOrder(index, offset, x, offsetLeft) {
        const children = this.#refs.header.children;
        let element = children[index];
        this.#refs.dragger.style.left = `${element.offsetLeft - offsetLeft + offset}px`;
        this.#refs.dragger.style.width = element.style.width;
        this.#refs.dragger.style.display = 'block';
        offset = x - getOffsetLeftFromWindow(element);
        let idx;
        if (offset < 0) {
            offset = -offset;
            for (let i = index - 1; i >= 0 && offset >= 0; i -= 1) {
                element = children[i];
                if (element == null || element.className !== 'column') {
                    break;
                }
                if (offset < element.offsetWidth) {
                    idx = (offset > element.offsetWidth / 2) ? i : i + 1;
                    break;
                }
                offset -= element.offsetWidth;
            }
            idx ??= 0;
        } else {
            const count = children.length;
            for (let i = index; i < count - 1 && offset >= 0; i += 1) {
                element = children[i];
                if (element == null || element.className !== 'column') {
                    idx = i;
                    break;
                }
                if (offset < element.offsetWidth) {
                    idx = (offset > element.offsetWidth / 2) ? i + 1 : i;
                    break;
                }
                offset -= element.offsetWidth;
            }
            idx ??= count - 1;
        }
        if (idx !== this.#colAttrs.__orderIndex) {
            this.#colAttrs.__orderIndex = idx;
            element = children[idx];
            if (element == null) {
                return;
            }
            this.#refs.draggerCursor.style.left = `${element.offsetLeft - offsetLeft}px`;
            this.#refs.draggerCursor.style.display = 'block';
        }
    }

    #changeColumnOrder(index) {
        this.#refs.dragger.style.display = '';
        this.#refs.draggerCursor.style.display = '';
        const orderIndex = this.#colAttrs.__orderIndex;
        if (orderIndex >= 0 && orderIndex !== index) {
            let targetIndex = orderIndex - index;
            if (targetIndex >= 0 && targetIndex <= 1) {
                return;
            }
            const header = this.#refs.header;
            const children = header.children;
            const rows = this.#refs.bodyContent.children;
            const columns = this.columns;
            if (targetIndex > 1) {
                targetIndex = orderIndex - 1;
                // const current = columns[index];
                // for (let i = index; i < targetIndex; i += 1) {
                //     columns[i] = columns[i + 1];
                // }
                // columns[targetIndex] = current;
                const current = columns.splice(index, 1)[0];
                columns.splice(targetIndex, 0, current);
                header.insertBefore(children[index], children[targetIndex].nextElementSibling);
                for (let row of rows) {
                    row.insertBefore(row.children[index], row.children[targetIndex].nextElementSibling);
                }
            } else {
                targetIndex = orderIndex;
                // const current = columns[index];
                // for (let i = index; i > targetIndex; i -= 1) {
                //     columns[i] = columns[i - 1];
                // }
                // columns[targetIndex] = current;
                const current = columns.splice(index, 1)[0];
                columns.splice(targetIndex, 0, current);
                header.insertBefore(children[index], children[targetIndex]);
                for (let row of rows) {
                    row.insertBefore(row.children[index], row.children[targetIndex]);
                }
            }
            // refresh sortIndex
            [...children].forEach((th, i) => {
                const arrow = th.querySelector('.arrow');
                if (arrow == null) {
                    return;
                }
                if (arrow.className !== 'arrow') {
                    this.sortIndex = i;
                }
            });

            if (typeof this.columnChanged === 'function') {
                this.columnChanged(ColumnChangedType.Reorder, index, targetIndex);
            }
        }
    }

    #scrollToTop(top, reload) {
        const rowHeight = (this.rowHeight + 1);
        top -= (top % (rowHeight * 2)) + (RedumCount * rowHeight);
        if (top < 0) {
            top = 0;
        } else {
            let bottomTop = this.#containerHeight - (reload ? 0 : this.#rowCount * rowHeight);
            if (bottomTop < 0) {
                bottomTop = 0;
            }
            if (top > bottomTop) {
                top = bottomTop;
            }
        }
        if (this.#scrollTop !== top) {
            this.#scrollTop = top;
            if (this.virtual) {
                this.#startIndex = top / rowHeight;
            }
            this.refresh();
            if (this.virtual) {
                this.#refs.bodyContent.style.top = `${top}px`;
            }
        } else if (reload) {
            this.refresh();
        }

        return top;
    }

    #get(key, name) {
        const attr = this.#colAttrs[key];
        if (attr == null) {
            return null;
        }
        return attr[name];
    }

    #set(key, name, value) {
        const attr = this.#colAttrs[key];
        if (attr == null) {
            this.#colAttrs[key] = { [name]: value };
        } else {
            attr[name] = value;
        }
    }

    #getItemValue(item, key, filter) {
        let value;
        if (typeof filter === 'function') {
            value = filter(item);
        } else {
            value = item[key];
        }
        return value?.value ?? value;
    }

    #getRowTarget(target) {
        let parent;
        while ((parent = target.parentElement) != null && !parent.classList.contains('ui-grid-row')) {
            target = parent;
        }
        return [parent, target];
    }

    #notHeader(tagName) {
        return /^(input|label|layer|svg|use)$/i.test(tagName);
    }

    #onHeaderClicked(e, col, force) {
        if (!force && (this.#get(col.key, 'resizing') || this.#get(col.key, 'dragging'))) {
            return;
        }
        if (!this.#notHeader(e.target.tagName)) {
            const index = this.columns.indexOf(col);
            if (index < 0) {
                return;
            }
            if (this.sortIndex === index) {
                this.sortDirection = this.sortDirection === 1 ? -1 : 1;
            } else {
                this.sortIndex = index;
            }
            this.sortColumn(true);
            if (typeof this.columnChanged === 'function') {
                this.columnChanged(ColumnChangedType.Sort, index, this.sortDirection);
            }
        }
    }

    #onCloseFilter() {
        const panels = this.#el.querySelectorAll('.filter-panel.active');
        if (panels.length > 0) {
            panels.forEach(el => el.classList.remove('active'));
            setTimeout(() => this.#el.querySelectorAll('.filter-panel').forEach(el => el.remove()), 120);
            const filtering = this.#colAttrs.__filtering;
            if (filtering instanceof HTMLElement) {
                filtering.classList.remove('hover');
            }
            delete this.#colAttrs.__filtering;
            return true;
        }
        return false;
    }

    #onFilter(e, col) {
        if (this.#onCloseFilter()) {
            return;
        }
        const close = e => {
            if ((e.target.tagName === 'LAYER' && e.target.classList.contains('filter')) ||
                e.target.tagName === 'use') {
                return;
            }
            if (this.#onCloseFilter()) {
                document.removeEventListener('mousedown', close);
            }
        }
        document.addEventListener('mousedown', close);
        const panel = createElement('div', 'filter-panel');
        panel.addEventListener('mousedown', e => e.stopPropagation());
        const filter = e.currentTarget;
        const th = filter.parentElement;
        const width = th.offsetWidth;
        panel.style.top = `${th.offsetHeight}px`;
        panel.style.left = (th.offsetLeft + (width > FilterPanelWidth ? width - FilterPanelWidth : 0)) + 'px';

        // search
        let searchbox;
        if (col.allowSearch !== false) {
            const searchholder = createElement('div', 'filter-search-holder');
            searchbox = createElement('input', 'filter-search-box ui-text');
            searchbox.type = 'text';
            const searchicon = createIcon('fa-regular', 'search');
            searchicon.addEventListener('mousedown', e => {
                searchbox.focus();
                e.preventDefault();
            });
            searchholder.append(searchbox, searchicon);
            panel.append(searchholder);
        }
        // list
        const itemlist = createElement('div', 'filter-item-list');
        itemlist.addEventListener('scroll', e => throttle(this.#onFilterScroll, RefreshInterval, this, col, itemlist, e.target.scrollTop), { passive: true });
        // - all
        const itemall = createElement('div', 'filter-item filter-all');
        itemall.appendChild(createCheckbox({
            label: this.langs.all,
            onchange: e => {
                const checked = e.target.checked;
                itemlist.querySelectorAll('.filter-content input').forEach(box => box.checked = checked);
            }
        }));
        itemlist.appendChild(itemall);
        // - items
        let array;
        if (Array.isArray(col.filterSource)) {
            array = col.filterSource;
        } else if (typeof col.filterSource === 'function') {
            array = col.filterSource.call(this, col);
        } else {
            const dict = Object.create(null);
            for (let item of this.#source) {
                const val = this.#getItemValue(item.values, col.key, col.filter);
                if (!Object.hasOwnProperty.call(dict, val)) {
                    const v = item.values[col.key];
                    dict[val] = {
                        value: val,
                        displayValue: typeof col.filter === 'function' ? col.filter(item.values) : v?.displayValue ?? v
                    };
                }
            }
            array = Object.values(dict)
                .sort((a, b) => {
                    a = a?.value ?? a;
                    b = b?.value ?? b;
                    return a > b ? 1 : a < b ? -1 : 0;
                });
        }
        array = array.map(i => {
            if (Object.prototype.hasOwnProperty.call(i, 'value') &&
                Object.prototype.hasOwnProperty.call(i, 'displayValue')) {
                return i;
            }
            return {
                value: i,
                displayValue: i == null ? '' : i
            };
        });
        this.#fillFilterList(col, itemlist, array, itemall);
        itemall.querySelector('input').checked = ![...itemlist.querySelectorAll('.filter-content input')].some(i => !i.checked);
        panel.appendChild(itemlist);
        if (searchbox != null) {
            searchbox.addEventListener('input', e => {
                const key = e.currentTarget.value.toLowerCase();
                const items = key.length === 0 ? array : array.filter(i => {
                    const displayValue = i?.displayValue ?? i;
                    return String(displayValue ?? '').indexOf(key) >= 0;
                });
                this.#fillFilterList(col, itemlist, items, itemall);
            });
        }
        // function
        const functions = createElement('div', 'filter-function');
        functions.append(
            createElement('button', ok => {
                ok.innerText = this.langs.ok;
                ok.addEventListener('click', () => {
                    const array = this.#get(col.key, 'filterSource').filter(i => i.__checked !== false);
                    if (typeof col.onFilterOk === 'function') {
                        col.onFilterOk.call(this, col, array);
                    } else {
                        col.filterValues = array.map(a => a.value);
                    }
                    this.#colAttrs.__filtered = true;
                    this.#refreshSource();
                    if (typeof col.onFiltered === 'function') {
                        col.onFiltered.call(this, col);
                    }
                    filter.classList.add('active');
                    this.#onCloseFilter();
                });
            }),
            createElement('button', reset => {
                reset.innerText = this.langs.reset;
                reset.addEventListener('click', () => {
                    delete col.filterValues;
                    this.#colAttrs.__filtered = this.columns.some(c => col.filterValues != null)
                    this.#refreshSource();
                    if (typeof col.onFiltered === 'function') {
                        col.onFiltered.call(this, col);
                    }
                    filter.classList.remove('active');
                    this.#onCloseFilter();
                });
            })
        );
        panel.appendChild(functions);

        this.#el.appendChild(panel);
        setTimeout(() => panel.classList.add('active'), 0);
        this.#colAttrs.__filtering = filter;
        filter.classList.add('hover');
    }

    #fillFilterList(col, list, array, all) {
        list.querySelector('.filter-holder')?.remove();
        list.querySelector('.filter-content')?.remove();
        const rowHeight = this.filterRowHeight;
        const height = array.length * rowHeight;
        this.#set(col.key, 'filterHeight', height);
        const holder = createElement('div', 'filter-holder');
        holder.style.height = `${height}px`;
        const content = createElement('div', 'filter-content');
        content.style.top = `${rowHeight}px`;
        this.#set(col.key, 'filterSource', array);
        for (let item of array) {
            item.__checked = !Array.isArray(col.filterValues) || col.filterValues.indexOf(item.value ?? item) >= 0;
        }
        if (array.length > 12) {
            array = array.slice(0, 12);
        }
        this.#doFillFilterList(content, array, all);
        list.append(holder, content);
    }

    #doFillFilterList(content, array, all) {
        for (let item of array) {
            const div = createElement('div', 'filter-item');
            div.appendChild(createCheckbox({
                checked: item.__checked,
                label: item?.displayValue ?? item,
                onchange: e => {
                    item.__checked = e.target.checked;
                    all.querySelector('input').checked = ![...content.querySelectorAll('input')].some(i => !i.checked);
                }
            }));
            content.appendChild(div);
        }
    }

    #onFilterScroll(col, list, top) {
        const rowHeight = this.filterRowHeight;
        top -= (top % (rowHeight * 2)) + rowHeight;
        if (top < 0) {
            top = 0;
        } else {
            let bottomTop = this.#get(col.key, 'filterHeight') - (12 * rowHeight);
            if (bottomTop < 0) {
                bottomTop = 0;
            }
            if (top > bottomTop) {
                top = bottomTop;
            }
        }
        if (this.#get(col.key, 'filterTop') !== top) {
            this.#set(col.key, 'filterTop', top);
            const startIndex = top / rowHeight;
            let array = this.#get(col.key, 'filterSource');
            if (startIndex + 12 < array.length) {
                array = array.slice(startIndex, startIndex + 12);
            } else {
                array = array.slice(-12);
            }
            const content = list.querySelector('.filter-content');
            content.replaceChildren();
            this.#doFillFilterList(content, array, list.querySelector('.filter-all>input'));
            content.style.top = `${top + rowHeight}px`;
        }
    }

    #onDragStart(e, col) {
        if (this.#notHeader(e.target.tagName)) {
            return;
        }
        const cx = getClientX(e);
        const index = indexOfParent(e.currentTarget);
        const clearEvents = attr => {
            for (let event of ['mousemove', 'mouseup']) {
                if (attr.hasOwnProperty(event)) {
                    window.removeEventListener(event, attr[event]);
                    delete attr[event];
                }
            }
        };
        let attr = this.#colAttrs[col.key];
        if (attr == null) {
            attr = this.#colAttrs[col.key] = {};
        } else {
            clearEvents(attr);
        }
        attr.dragging = true;
        const offsetLeft = this.#refs.header.querySelector('th:last-child').offsetLeft;
        const dragmove = e => {
            const cx2 = getClientX(e);
            const offset = cx2 - cx;
            let pos = attr.offset;
            let dragging;
            if (pos == null && (offset > MiniDragOffset || offset < -MiniDragOffset)) {
                dragging = true;
            } else if (pos !== offset) {
                dragging = true;
            }
            if (dragging) {
                this.#changingColumnOrder(index, offset, cx2, offsetLeft);
                attr.offset = offset;
            }
        };
        attr.mousemove = e => throttle(dragmove, RefreshInterval, this, e);
        attr.mouseup = () => {
            clearEvents(attr);
            if (attr.offset == null) {
                delete attr.dragging;
            } else {
                setTimeout(() => {
                    delete attr.dragging;
                    delete attr.offset;
                });
                this.#changeColumnOrder(index);
            }
        };
        ['mousemove', 'mouseup'].forEach(event => window.addEventListener(event, attr[event]));
    }

    #onResizeStart(e, col) {
        const cx = getClientX(e);
        const width = col.width;
        const index = indexOfParent(e.currentTarget.parentElement);
        const window = this.window ?? global;
        const clearEvents = attr => {
            for (let event of ['mousemove', 'mouseup']) {
                if (attr.hasOwnProperty(event)) {
                    window.removeEventListener(event, attr[event]);
                    delete attr[event];
                }
            }
        };
        let attr = this.#colAttrs[col.key];
        if (attr == null) {
            attr = this.#colAttrs[col.key] = {};
        } else {
            clearEvents(attr);
        }
        attr.resizing = width;
        const resizemove = e => {
            const cx2 = getClientX(e);
            const val = width + (cx2 - cx);
            if (val < MiniColumnWidth) {
                return;
            }
            attr.resizing = val;
            attr.sizing = true;
            this.#changeColumnWidth(index, val);
        };
        attr.mousemove = e => throttle(resizemove, RefreshInterval, this, e);
        attr.mouseup = e => {
            clearEvents(attr);
            const width = attr.resizing;
            if (width != null) {
                setTimeout(() => delete attr.resizing);
                if (attr.sizing) {
                    delete attr.sizing;
                    delete attr.autoResize;
                    this.#changeColumnWidth(index, width);
                    if (typeof this.columnChanged === 'function') {
                        this.columnChanged(ColumnChangedType.Resize, index, width);
                    }
                }
            }
            e.stopPropagation();
            e.preventDefault();
        };
        ['mousemove', 'mouseup'].forEach(event => window.addEventListener(event, attr[event]));
    }

    #onAutoResize(e, col) {
        const th = e.currentTarget.parentElement;
        const index = indexOfParent(th);
        let width = th.querySelector('div:first-child').scrollWidth;
        for (let row of this.#refs.bodyContent.children) {
            const element = row.children[index].children[0];
            const w = element.scrollWidth;
            if (w > width) {
                width = w;
            }
        }
        if (width < MiniColumnWidth) {
            width = MiniColumnWidth;
        }
        if (width > 0 && width !== col.width) {
            width += 12;
            this.#changeColumnWidth(index, width);
            if (typeof this.columnChanged === 'function') {
                this.columnChanged(ColumnChangedType.Resize, index, width);
            }
        }
    }

    #onColumnAllChecked(col, flag) {
        if (this.#currentSource == null) {
            return;
        }
        const key = col.key;
        const isFunction = typeof col.enabled === 'function';
        const isString = typeof col.enabled === 'string';
        if (typeof col.onallchecked === 'function') {
            col.onallchecked.call(this, col, flag);
        } else {
            for (let row of this.#currentSource) {
                const item = row.values;
                if (item == null) {
                    continue;
                }
                const enabled = isFunction ? col.enabled(item) : isString ? item[col.enabled] : col.enabled;
                if (enabled !== false) {
                    item[key] = flag;
                    row.__changed = true;
                    if (typeof col.onchanged === 'function') {
                        col.onchanged.call(this, item, flag);
                    }
                }
            }
            this.refresh();
        }
    }

    #onScroll(e) {
        const left = e.target.scrollLeft;
        if (this.#scrollLeft !== left) {
            this.#scrollLeft = left;
            this.#refs.header.style.left = `${-left}px`;
        }
        if (!this.virtual) {
            return;
        }
        const top = e.target.scrollTop;
        this.#scrollToTop(top);
    }

    #onBodyMouseMove(e, holder) {
        if (e.target.classList.contains('ui-grid-hover-holder')) {
            return;
        }
        let [parent, target] = this.#getRowTarget(e.target);
        if (parent == null) {
            delete holder.dataset.row;
            delete holder.dataset.col;
            if (holder.classList.contains('active')) {
                holder.classList.remove('active');
            }
            return;
        }
        const element = target.children[0];
        if (element?.tagName !== 'SPAN') {
            if (holder.classList.contains('active')) {
                delete holder.dataset.row;
                delete holder.dataset.col;
                holder.classList.remove('active');
            }
            return;
        }
        const row = target.dataset.row;
        const col = target.dataset.col;
        if (holder.dataset.row === row &&
            holder.dataset.col === col) {
            return;
        }
        if (element.scrollWidth > element.offsetWidth) {
            holder.dataset.row = row;
            holder.dataset.col = col;
            holder.innerText = element.innerText;
            const top = this.#refs.bodyContent.offsetTop + target.offsetTop;
            let left = target.offsetLeft;
            let width = holder.offsetWidth;
            if (width > this.#bodyClientWidth) {
                width = this.#bodyClientWidth;
            }
            const maxleft = this.#bodyClientWidth + this.#scrollLeft - width;
            if (left > maxleft) {
                left = maxleft;
            }
            const height = target.offsetHeight;
            holder.style.cssText = `top: ${top}px; left: ${left}px; max-width: ${this.#bodyClientWidth}px; height: ${height - 2}px`;
            holder.classList.add('active');
        } else if (holder.classList.contains('active')) {
            delete holder.dataset.row;
            delete holder.dataset.col;
            holder.classList.remove('active');
        }
    }

    #onRowClicked(e, index, colIndex) {
        const startIndex = this.#startIndex;
        const selectedIndex = startIndex + index;
        if (typeof this.willSelect === 'function' && !this.willSelect(selectedIndex, colIndex)) {
            return;
        }
        // multi-select
        let flag = false;
        const selectedIndexes = this.#selectedIndexes;
        if (this.multiSelect) {
            if (e.ctrlKey) {
                const i = selectedIndexes.indexOf(selectedIndex);
                if (i < 0) {
                    selectedIndexes.push(selectedIndex);
                } else {
                    selectedIndexes.splice(i, 1);
                }
                flag = true;
            } else if (e.shiftKey && selectedIndexes.length > 0) {
                if (selectedIndexes.length > 1 || selectedIndexes[0] !== selectedIndex) {
                    let start = selectedIndexes[selectedIndexes.length - 1];
                    let end;
                    if (start > selectedIndex) {
                        end = start;
                        start = selectedIndex;
                    } else {
                        end = selectedIndex;
                    }
                    selectedIndexes.splice(0);
                    for (let i = start; i <= end; i += 1) {
                        selectedIndexes.push(i);
                    }
                    flag = true;
                }
            }
        }
        if (!flag && selectedIndexes.length !== 1 || selectedIndexes[0] !== selectedIndex) {
            selectedIndexes.splice(0, selectedIndexes.length, selectedIndex);
            flag = true;
        }
        // apply style
        if (flag) {
            if (this.readonly !== true) {
                this.refresh();
            } else {
                [...this.#refs.bodyContent.children].forEach((row, i) => {
                    if (selectedIndexes.indexOf(startIndex + i) >= 0) {
                        row.classList.add('selected');
                    } else if (row.classList.contains('selected')) {
                        row.classList.remove('selected');
                    }
                });
            }
            if (typeof this.selectedRowChanged === 'function') {
                this.selectedRowChanged(selectedIndex);
            }
        }
        this.#selectedColumnIndex = colIndex;
        if ((this.fullrowClick || colIndex >= 0) && e.buttons === 1 && typeof this.cellClicked === 'function') {
            if (this.cellClicked(selectedIndex, colIndex) === false) {
                e.stopPropagation();
                e.preventDefault();
            }
        }
    }

    #onRowDblClicked(e) {
        if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA' || e.target.tagName === 'LAYER' && e.target.className === 'ui-check-inner' || e.target.tagName === 'LABEL' && (e.target.className === 'ui-drop-text' || e.target.className === 'ui-drop-caret')) {
            return;
        }
        const index = this.selectedIndex;
        if (typeof this.rowDblClicked === 'function') {
            this.rowDblClicked(index);
        }
        if (typeof this.cellDblClicked === 'function') {
            const colIndex = this.#selectedColumnIndex;
            if (this.fullrowClick || colIndex >= 0) {
                this.cellDblClicked(index, colIndex);
            }
        }
    }

    #onRowChanged(_e, index, col, value, cell, blur) {
        if (this.#currentSource == null) {
            return;
        }
        const row = this.#currentSource[this.#startIndex + index];
        const item = row.values;
        if (item == null) {
            return;
        }
        let enabled = col.enabled;
        if (typeof enabled === 'function') {
            enabled = enabled.call(col, item);
        } else if (typeof enabled === 'string') {
            enabled = item[enabled];
        }
        if (enabled !== false) {
            item[col.key] = value;
            let tip = col.tooltip;
            if (typeof tip === 'function') {
                tip = tip.call(col, item);
            }
            if (nullOrEmpty(tip)) {
                cell.querySelector('.ui-tooltip-wrapper')?.remove();
            } else {
                setTooltip(cell.children[0], tip, false, this.element);
            }
            row.__changed = true;
            if (blur) {
                if (typeof col.oneditend === 'function') {
                    col.oneditend.call(this, item, value);
                }
            } else {
                if (typeof col.onchanged === 'function') {
                    col.onchanged.call(this, item, value);
                }
            }
        }
    }
}

export default Grid;