import { IgniteHtml, IgniteObject, IgniteProperty, IgniteRendering } from "../ignite-html/ignite-html.js";
import { IgniteElement } from "../ignite-html/ignite-element.js";
import { IgniteTemplate, div, table, thead, tbody, tr, th, td, list, pagination, pager, population, nav, ul, li, button, span, input, select, option, i } from "../ignite-html/ignite-template.js";

/**
 * The outline of a column apart of a DataTable.
 */
class DataColumn extends IgniteObject {
    /**
     * The data table this row belongs to.
     * @type {DataTable}
     */
    table;

    /** 
     * The name of the column, this can contain html.
     * @type {String|Any} 
     * */
    name = null;

    /**
     * A selector to extract the value from the data for this column.
     * @type {IgniteProperty|Function}
     */
    selector = null;

    /**
     * A function to convert the value of the column to a component.
     * @type {IgniteProperty|Function}
     */
    converter = null;

    /**
    * A custom comparer used for sorted, default is null.
    * @type {IgniteProperty|Function}
    */
    comparer = null;

    /**
     * A custom function used for searching, default is null.
     * @type {IgniteProperty|Function}
     */
    searcher = null;

    /**
     * Whether or not this column can be searched.
     * @type {Boolean}
     */
    searchable = false;

    /**
     * Whether or not this column can be sorted.
     * @type {Boolean}
     */
    sortable = false;

    /**
     * The current sorting of this column if any, default is null. Possible values are asc, desc, null.
     * @type {String}
     */
    sort = null;

    /**
     * A callback function that is invoked when the column is clicked.
     * @type {Function} The callback function to be called when this column is clicked. The instance of the column is passed as the first parameter.
     */
    onClick = null;

    /**
     * Creates a new DataColumn with a name and options.
     * @param {String} name The name of the column to display 
     * @param {Object} options The options for this data column.
     * @param {Boolean|Function} options.searchable Whether or not this column can be searched. Default is false.
     * @param {String|Function} options.sortable Whether or not this column can be sorted. Default is false.
     * @param {String} options.sort The current sorting of this column. Possible values: asc, desc. Default is null.
     * @param {IgniteProperty|Function} options.selector A function to extract the column value from given data. Default is null.
     * @param {IgniteProperty|Function} options.converter A function to convert the column value to a custom component. Default is null.
     * @param {IgniteProperty|Function} options.comparer A function to compare two columns for sorting. Default is null.
     * @param {IgniteProperty|Function} options.searcher A function to seatch a column. Default is null.
     */
    constructor(name, options = null) {
        super();

        this.name = name;

        if (options) {
            this.searchable = options.searchable ?? this.searchable;
            this.sortable = options.sortable ?? this.sortable;
            this.sort = options.sort ?? this.sort;
            this.selector = options.selector ?? this.selector;
            this.converter = options.converter ?? this.converter;
            this.comparer = options.comparer ?? this.comparer;
            this.searcher = options.searcher ?? this.searcher;
            this.onClick = options.onClick ?? this.onClick;

            if (this.sort == "asc" || this.sort == "desc") {
                this.sortable = true;
            }
        }

        this.update();
    }
}

/**
 * The outline of a row apart of the DataTable.
 */
class DataRow {
    /**
     * The data table this row belongs to.
     * @type {DataTable}
     */
    table;

    /**
     * The raw data of this row that is then
     * broken down into values and columns.
     * @type {Any}
     */
    data;

    /**
     * The raw values of this row.
     * @type {Array<Any>}
     */
    values;

    /**
     * The columns of this row that will be rendered.
     * @type {Array<Any>}
     */
    columns;

    /**
     * Creates a new DataRow from data a set of columns.
     * @param {DataTable} table
     * @param {Any} data 
     * @param {Array<DataColumn} columns 
     */
    constructor(table, data, columns) {
        this.table = table;

        this.data = data;

        this.values = [];

        this.columns = [];

        for (let i = 0; i < columns.length; i++) {
            var column = columns[i];

            var value = data;

            //We want to enter a rendering context for the selector so we have a chance to see if we are returned a property.
            IgniteRendering.enter();
            if (column.selector && column.selector instanceof IgniteProperty && column.selector.value instanceof Function) {
                value = column.selector.value(data);
            } else if (column.selector && column.selector instanceof Function) {
                value = column.selector(data);
            }
            IgniteRendering.leave();

            //Store this value for this column
            this.values.push(value);

            //If the value is a property, listen for changes and request reflitering of the table.
            if (value instanceof IgniteProperty) {
                value.attachOnChange(() => this.table.requestFilter());
            }

            //Invoke the column converter if there is one.
            if (column.converter instanceof Function) {
                var converter = column.converter;
                
                IgniteRendering.enter();
                this.columns.push(converter(value));
                IgniteRendering.leave();
            } else {
                this.columns.push(value);
            }
        }
    }
}

/**
 * A data table component that can render rows and columns and handle
 * searching and sorting along with pagination of a data set.
 */
class DataTable extends IgniteElement {
    constructor() {
        super();
    }

    get styles() {
        return /*css*/`
            /* Prevent selecting of column names */
            bt-data-table table thead th {
                user-select: none;
            }

            /* Round the corners of the table */
            bt-data-table table thead tr:last-child th:first-child {
                border-top-left-radius: var(--bs-border-radius);
            }

            bt-data-table table thead tr:last-child th:last-child {
                border-top-right-radius: var(--bs-border-radius);
            }

            bt-data-table table tbody tr:last-child td:first-child {
                border-bottom-left-radius: var(--bs-border-radius);
            }

            bt-data-table table tbody tr:last-child td:last-child {
                border-bottom-right-radius: var(--bs-border-radius);
            }
        `;
    }

    get properties() {
        return {
            columns: [],
            data: new IgniteProperty([], {
                onChange: (oldValue, newValue) => this.rows = (newValue ? newValue.map(row => new DataRow(this, row, this.columns)) : []),
                onPop: () => this.rows.pop(),
                onPush: (array, data) => this.rows.push(...data.map(row => new DataRow(this, row, this.columns))),
                onShift: () => this.rows.shift(),
                onSplice: (array, start, deleteCount, data) => this.rows.splice(start, deleteCount, ...data.map(row => new DataRow(this, row, this.columns))),
                onUnshift: (array, data) => this.rows.unshift(...data.map(row => new DataRow(this, row, this.columns)))
            }),
            rows: new IgniteProperty([], {
                onChange: () => this.filter(),
                onPop: () => this.filter(),
                onPush: () => this.filter(),
                onShift: () => this.filter(),
                onSplice: () => this.filter(),
                onUnshift: () => this.filter()
            }),
            filtered: new IgniteProperty([], {
                onChange: () => this.pageCount = this.filtered && this.pageSize > 0 ? Math.ceil(this.filtered.length / this.pageSize) : 0,
                onPop: () => this.pageCount = this.filtered && this.pageSize > 0 ? Math.ceil(this.filtered.length / this.pageSize) : 0,
                onPush: () => this.pageCount = this.filtered && this.pageSize > 0 ? Math.ceil(this.filtered.length / this.pageSize) : 0,
                onShift: () => this.pageCount = this.filtered && this.pageSize > 0 ? Math.ceil(this.filtered.length / this.pageSize) : 0,
                onSplice: () => this.pageCount = this.filtered && this.pageSize > 0 ? Math.ceil(this.filtered.length / this.pageSize) : 0,
                onUnshift: () => this.pageCount = this.filtered && this.pageSize > 0 ? Math.ceil(this.filtered.length / this.pageSize) : 0
            }),
            filterSearch: null,
            pageSizeOptions: [15, 25, 50, 100, 500],
            pageSize: new IgniteProperty(15, {
                onChange: (oldValue, newValue) => {
                    //Calculate the new page count based on the page size and data.
                    if (!this.data || newValue < 1) {
                        this.pageCount = 0;
                    } else {
                        this.pageCount = Math.ceil(this.data.length / newValue);
                    }
                }
            }),
            pageCount: new IgniteProperty(0, {
                onChange: (oldValue, newValue) => {
                    //If the currentPage is greater than the new page count, set it to the last page.
                    if (this.currentPage > newValue - 1) {
                        this.currentPage = newValue - 1;
                    }
                }
            }),
            currentPage: 0,
            headClass: "table-dark",
            bodyClass: null,
            pendingSearch: null,
            showPageSize: true,
            pageSizeClass: "form-select w-auto text-dark",
            pageSizeSpanClass: "input-group-text bg-white text-dark",
            showSearchBox: true,
            searchBoxClass: "form-control bg-white",
            searchBoxSpanClass: "input-group-text bg-white text-dark",
            showRowCount: true,
            rowCountClass: "bg-white p-2 rounded-2 flex-grow-1 flex-sm-grow-0 text-dark",
            showPages: true,
            showPageJumpTo: true,
            pageJumpToClass: "form-select border-0 w-auto text-dark",
            showRefreshButton: true,
            refreshButtonClass: "btn btn-secondary text-dark",
            refresh: null,
            pendingFilter: null,
            loading: false
        }
    }

    render() {
        return this.template.child(
            new div().show([this.showPageSize, this.showSearchBox, this.showRowCount, this.showRefreshButton], (pageSize, searchBox, rowCount, refreshButton) => pageSize || searchBox || rowCount || refreshButton).class("row g-3 mb-3").child(
                new div().class("col-12 col-sm-8 col-lg-8 d-flex flex-row justify-content-start gap-3 flex-nowrap").child(
                    //Page size selector
                    new div().show(this.showPageSize).class("input-group d-flex flex-nowrap w-auto flex-grow-1 flex-sm-grow-0").child(
                        new span().class(this.pageSizeSpanClass).child(`<i class="fa-solid fa-list-ul"></i>`),

                        new select().class(this.pageSizeClass).child(
                            new list(this.pageSizeOptions, size => new option().child(size))
                        ).value(this.pageSize, true)
                    ),

                    //Row count
                    new span().show(this.showRowCount).class(this.rowCountClass).innerHTML(this.filtered, filtered => filtered.length == 0 ? "No rows" : `${filtered.length} ${(filtered.length < 2 ? "row" : "rows")}`),

                    //Refresh button
                    new button().show(this.showRefreshButton).class(this.refreshButtonClass).child(new i().class("fa-solid fa-arrows-rotate")).onClick(async () => {
                        if (this.refresh instanceof Function) {
                            this.loading = true;

                            var refreshStart = performance.now();

                            await this.refresh();

                            //Add an artifial delay of 250ms at least unless the operation took longer.
                            await new Promise(r => setTimeout(r, Math.max(0, 250 - (performance.now() - refreshStart))));

                            this.loading = false;
                        }
                    }),
                ),

                //Search box
                new div().show(this.showSearchBox).class("col-12 col-sm-4 col-lg-4 d-flex flex-row justify-content-end gap-3 flex-wrap").child(
                    new div().class("input-group flex-nowrap w-auto flex-grow-1").child(
                        new span().class(this.searchBoxSpanClass).child(`<i class="fa-solid fa-magnifying-glass"></i>`),

                        new input().class(this.searchBoxClass)
                            .value(this.filterSearch, true)
                            .onEnter(() => this.filter())
                            .onChange(() => this.filter())
                            .on("keydown", (event) => {
                                if (event.key != 'Shift' && event.key != 'Capslock' && event.key != 'Space' && event.key != 'Control') {
                                    this.requestSearch();
                                }
                            })
                    )
                )
            ),

            //Rows
            new div().class("mb-3").child(
                new table().class("table table-light align-middle mb-0 table-borderless").child(
                    //Table columns
                    new thead().class(this.headClass).child(
                        new tr().child(
                            new list(this.columns, column => {
                                //Ensure the column table instance is set.
                                column.table = this;

                                return new th().class("text-nowrap").class(column.sortable, sortable => sortable ? "cursor-pointer" : null).attribute("scope", "col").child(
                                    new i().show([column.sortable, column.sort], (sortable, sort) => sortable && sort).class("me-2").class(column.sort, sort => sort ? (sort == "asc" ? "fa-solid fa-angle-up" : "fa-solid fa-angle-down") : null),
                                    column.name
                                ).onClick(() => {
                                    if (column.sortable) {
                                        if (column.sort == "asc") {
                                            column.sort = "desc";
                                            this.filter();
                                        } else if (column.sort == "desc") {
                                            column.sort = null;
                                            this.filter();
                                        } else if (!column.sort) {
                                            column.sort = "asc";
                                            this.filter();
                                        }
                                    }

                                    if (column.onClick) {
                                        column.onClick(column);
                                    }
                                });
                            })
                        )
                    ),

                    //Table rows
                    new tbody().class(this.bodyClass).hide(this.loading).child(
                        new pagination(this.filtered, this.pageSize, this.currentPage, row => new tr().child(new list(row.columns, column => new td().child(column))))
                    )
                ),

                //Loading spinner
                new div().show(this.loading).class("d-flex justify-content-center mt-3").child(
                    new div().class("spinner-border")
                )
            ),

            new div().class("row g-3").child(
                //Pagination pages
                new div().show(this.showPages).class("col-12 col-sm-9 col-lg-8 d-flex flex-row justify-content-start").child(
                    new div().class("btn-group flex-wrap justify-content-center d-flex flex-grow-1 flex-sm-grow-0").child(
                        //Previous page button
                        new button()
                            .class("btn btn-secondary flex-grow-1")
                            .child(`<i class="fa-solid fa-chevron-left"></i>`)
                            .onClick(() => {
                                if (this.currentPage > 0) {
                                    this.currentPage = this.currentPage - 1;
                                }
                            }),

                        new pager(
                            this.filtered,
                            this.pageSize,
                            this.currentPage,
                            2,
                            (index, current, filler) => {
                                if (filler) {
                                    return null;
                                }

                                return new button()
                                    .class("btn text-center flex-grow-1")
                                    .class(current, current => current ? "btn-primary" : "btn-secondary")
                                    .innerText(filler ? "..." : index + 1)
                                    .onClick(() => this.currentPage = index)
                            }),

                        //Next page button
                        new button()
                            .class("btn btn-secondary flex-grow-1")
                            .child(`<i class="fa-solid fa-chevron-right"></i>`)
                            .onClick(() => {
                                if (this.currentPage < this.pageCount) {
                                    this.currentPage = this.currentPage + 1;
                                }
                            }),
                    )
                ),

                //Page jump to select
                new div().show(this.showPageJumpTo).class("col-12 col-sm-3 col-lg-4 d-flex flex-row justify-content-end").child(
                    new div().class("input-group flex-nowrap w-auto flex-grow-1 flex-sm-grow-0").child(
                        new select().class(this.pageJumpToClass).child(
                            new population(
                                this.pageCount,
                                index => new option().attribute("selected", this.currentPage, currentPage => currentPage == index ? "selected" : null).value(index).innerText(`Page ${index + 1}`)
                            )
                        ).value(this.currentPage, true)
                    )
                )
            )
        );
    }

    filter() {
        var filtered = [];

        filtered = filtered.concat(this.rows);

        //First filter the results by the search query if we have one.
        if (this.filterSearch && this.filterSearch != "" && this.filterSearch != " ") {
            var regex = new RegExp(this.filterSearch, 'i');

            filtered = filtered.filter((/** @type {DataRow} **/ row) => {
                for (var i = 0; i < this.columns.length; i++) {
                    var value = row.values[i] instanceof IgniteProperty ? row.values[i].value : row.values[i];

                    if (this.columns[i].searchable && value && value.match(regex)) {
                        return true;
                    }
                }

                return false;
            });
        }

        //Apply any sorting if needed
        filtered.sort((/** @type {DataRow} **/ a, /** @type {DataRow} **/ b) => {
            for (var i = 0; i < this.columns.length; i++) {
                if (this.columns[i].sortable) {
                    var aValue = a.values[i] instanceof IgniteProperty ? a.values[i].value : a.values[i];
                    var bValue = b.values[i] instanceof IgniteProperty ? b.values[i].value : b.values[i];

                    //Sort if we are sorting by ascending or descending
                    if (this.columns[i].sort == "asc") {
                        if (!aValue && bValue) {
                            return -1;
                        } else if (!bValue && aValue) {
                            return 1;
                        } else if (aValue && bValue && aValue > bValue) {
                            return 1;
                        } else if (aValue && bValue && aValue < bValue) {
                            return -1;
                        }
                    } else if (this.columns[i].sort == "desc") {
                        if (!aValue && bValue) {
                            return 1;
                        } else if (!bValue && aValue) {
                            return -1;
                        } else if (aValue && bValue && aValue > bValue) {
                            return -1;
                        } else if (aValue && bValue && aValue < bValue) {
                            return 1;
                        }
                    }
                }
            }

            return 0;
        });

        this.filtered = filtered;
    }

    requestSearch() {
        if (this.pendingSearch) {
            clearTimeout(this.pendingSearch);
        }

        this.pendingSearch = setTimeout(() => {
            this.pendingSearch = null;
            this.filter();
        }, 100);
    }

    requestFilter() {
        if (this.pendingFilter) {
            clearTimeout(this.pendingFilter);
        }

        this.pendingFilter = setTimeout(() => {
            this.pendingFilter = null;
            this.filter();
        }, 10);
    }
}

class Template extends IgniteTemplate {
    /**
     * Creates a new data table with the columns and data to display.
     * @param {Array<DataColumn>} columns The columns of this table
     * @param {Array<any>} data The data of this table
     */
    constructor(columns, data, converter) {
        super("bt-data-table", null);

        this.property("columns", columns);
        this.property("data", data);
    }
}

IgniteHtml.register("bt-data-table", DataTable);

export {
    Template as DataTable,
    DataColumn,
    DataRow
}