import { css, html } from "lit";
import { ifDefined } from "lit/directives/if-defined.js";
import { classMap } from "lit/directives/class-map.js";
import { dataAttr } from "../../directives/data-attr.js";
import '../../define/ui/ui-divider.js';
/**
 * A helper class that allows to build a data table in a programmatic way.
 *
 * Usage:
 *
 * const dt = new DataTable(arrayOfItems, { key: 'key' });
 * // or dt.items = arrayOfItems;
 * dt.addEventListener('header', () => {
 *  const c1 = dt.header('name', { class: 'name-column', data: {a: b} });
 *  const c2 = dt.header('created', { class: 'created-column', data: {a: b} });
 *  return [c1, c2];
 * });
 * dt.addEventListener('item', (item) => {
 *   const c1 = dt.cell(html`cell template content`, { class: 'a b c', data: {a: b} });
 *   const c2 = dt.cell(`cell text content`, { class: 'xyz', data: {key: 'value'} });
 *   return [c1, c2];
 * });
 * dt.addEventListener('empty', () => {
 *   return html`<p>The table has no records.</p>`;
 * });
 *
 * const template = dt.render();
 *
 * @fires header - An event dispatched when a table header row is being rendered.
 * @fires item - An event dispatched when a table item row is being rendered.
 * @fires empty - An event dispatched when a table has no rows to render.
 * @fires activate - An event dispatched when the user double click on a row.
 * @fires select - An event dispatched when the user selected a table item in a single or multi selection table.
 * @fires render - An event dispatched when the internal state changed and the table must be rendered again.
 */
export class DataTable {
    static get styles() {
        return css `
    .data-table {
      flex: 1;
      display: flex;
      flex-direction: column;
      overflow: hidden;
    }

    .data-table-body {
      flex: 1;
      overflow: auto;
    }

    .data-table-row {
      height: 44px;
      position: relative;

      display: flex;
      align-items: center;

      font-family: var(--md-sys-typescale-body-medium-font-family-name);
      font-style: var(--md-sys-typescale-body-medium-font-family-style);
      font-weight: var(--md-sys-typescale-body-medium-font-weight);
      font-size: var(--md-sys-typescale-body-medium-font-size);
      letter-spacing: var(--md-sys-typescale-body-medium-tracking);
      line-height: var(--md-sys-typescale-body-medium-height);
      text-transform: var(--md-sys-typescale-body-medium-text-transform);
      text-decoration: var(--md-sys-typescale-body-medium-text-decoration);
    }

    .data-table-row-body {
      height: inherit;
      display: flex;
      align-items: center;
      flex: 1;
    }

    .data-table-body .data-table-row:not(.data-table-empty-row):hover::before {
      content: '';
      background-color: var(--md-sys-color-on-surface);
      opacity: var(--md-sys-state-hover-state-layer-opacity);
      position: absolute;
      inset: 0;
      z-index: -1;
    }

    .data-table-header {
      font-family: var(--md-sys-typescale-label-large-font-family-name);
      font-style: var(--md-sys-typescale-label-large-font-family-style);
      font-weight: var(--md-sys-typescale-label-large-font-weight);
      font-size: var(--md-sys-typescale-label-large-font-size);
      letter-spacing: var(--md-sys-typescale-label-large-tracking);
      line-height: var(--md-sys-typescale-label-large-height);
      text-transform: var(--md-sys-typescale-label-large-text-transform);
      text-decoration: var(--md-sys-typescale-label-large-text-decoration);
    }

    .data-table-cell {
      display: flex;
      align-items: center;
      overflow: hidden;
      text-overflow: ellipsis;
      white-space: nowrap;
      height: inherit;
      padding: 12px;
      box-sizing: border-box;
      position: relative;
    }

    .data-table-row.data-table-empty-row {
      height: auto;
      display: block;
    }

    .data-table-row.data-table-empty-row > [role=cell] {
      display: flex;
      flex-direction: column;
      align-items: center;
      justify-content: center;
    }

    .selected {
      background-color: var(--md-sys-color-secondary-container);
      border-radius: 8px;
    }

    .data-table-cell:focus-visible::before {
      content: '';
      background-color: var(--md-sys-color-on-surface);
      opacity: var(--md-sys-state-focus-state-layer-opacity);
      position: absolute;
      inset: 0;
      z-index: -1;
    }
    `;
    }
    #renderItems;
    get items() {
        return this.#renderItems;
    }
    set items(value) {
        if (!value) {
            this.#renderItems = undefined;
            return;
        }
        const arr = Array.from(value);
        this.#renderItems = arr;
    }
    #listeners;
    #getItems;
    constructor(itemsCallback, options = {}) {
        this.#listeners = {};
        /**
         * The list of keys that correspond to the `key` set on the options of items that are currently selected.
         * Note, this is a common list for single and multi selection. In a single selection only the first is taken into account.
         */
        this.selected = [];
        this.options = options;
        this.#getItems = itemsCallback;
        this.dblclickHandler = this.dblclickHandler.bind(this);
        this.handleClick = this.handleClick.bind(this);
        this.handleKeyDown = this.handleKeyDown.bind(this);
        this.handleFocus = this.handleFocus.bind(this);
        this.handleListScroll = this.handleListScroll.bind(this);
    }
    addEventListener(type, listener) {
        this.#listeners[type] = listener;
    }
    /**
     * Renders a header cell.
     *
     * @param content The content of the header cell to render.
     * @param options Cell rendering options
     */
    header(content, options = {}) {
        const data = options.data || {};
        return html `<span 
      role="columnheader" 
      class="data-table-cell data-table-header-cell ${options.class || ''}"
      aria-label="${ifDefined(options.ariaLabel)}"
      ${dataAttr(data)}
    >${content}</span>`;
    }
    /**
     * Renders a body cell.
     *
     * @param content The content of the header cell to render.
     * @param options Cell rendering options
     */
    cell(content, options = {}) {
        const data = options.data || {};
        return html `
    <span 
      role="gridcell" 
      class="data-table-cell data-table-body-cell ${options.class || ''}"
      ${dataAttr(data)}
    >${content}</span>
    `;
    }
    render() {
        this.items = this.#getItems();
        const { options } = this;
        const clickListener = options.selection ? this.handleClick : undefined;
        return html `
    <div 
      class="data-table" 
      role="grid" 
      aria-label="${ifDefined(options.ariaLabel)}"
      aria-multiselectable="${options.selection === 'multi'}"
      aria-readonly="true"
      aria-rowcount="-1"
      tabindex="0"
      @focus="${this.handleFocus}"
      @click="${clickListener}" 
      @keydown="${this.handleKeyDown}"
    >
      ${this.renderHeader()}
      <ui-divider></ui-divider>
      ${this.renderBody()}
    </div>
    `;
    }
    rowFromEvent(e) {
        const path = e.composedPath();
        const row = path.find(current => {
            const element = current;
            if (element.classList && element.classList.contains('data-table-row')) {
                return true;
            }
            return false;
        });
        if (!row || !row.dataset.key) {
            return undefined;
        }
        return row;
    }
    tableFromEvent(e) {
        const path = e.composedPath();
        const row = path.find((current) => {
            const element = current;
            if (element.classList && element.classList.contains('data-table')) {
                return true;
            }
            return false;
        });
        return row;
    }
    cellPositionFromEvent(e) {
        const result = {
            cell: 0,
            row: 0,
        };
        let row;
        let column;
        let table;
        const path = e.composedPath();
        for (const current of path.values()) {
            const element = current;
            if (element.classList && element.classList.contains('data-table-row')) {
                row = element;
            }
            else if (element.classList && element.classList.contains('data-table-cell')) {
                column = element;
            }
            else if (element.classList && element.classList.contains('data-table')) {
                table = element;
                break;
            }
        }
        if (!table) {
            return result;
        }
        if (row) {
            const rows = Array.from(table.querySelectorAll(`.data-table-row`));
            const rowIndex = rows.indexOf(row);
            if (rowIndex >= 0) {
                result.row = rowIndex;
            }
            if (column) {
                const columns = Array.from(row.querySelectorAll(`.data-table-cell`));
                const cellIndex = columns.indexOf(column);
                if (cellIndex >= 0) {
                    result.cell = cellIndex;
                }
            }
        }
        return result;
    }
    cellRowFromEvent(e) {
        let row;
        let column;
        const path = e.composedPath();
        for (const current of path.values()) {
            const element = current;
            if (element.classList && element.classList.contains('data-table-row')) {
                row = element;
                break;
            }
            else if (element.classList && element.classList.contains('data-table-cell')) {
                column = element;
            }
        }
        if (!row || !column) {
            return undefined;
        }
        return { row, column };
    }
    handleFocus(e) {
        const target = e.target;
        if (target.classList.contains('data-table')) {
            // focusing on the table, move the focus to the first cell
            this.focusFirstCell(target);
        }
    }
    dblclickHandler(e) {
        const callback = this.#listeners.activate;
        if (!callback) {
            return;
        }
        const row = this.rowFromEvent(e);
        if (!row) {
            return;
        }
        const { key } = row.dataset;
        const keyProperty = this.options.key || 'key';
        const item = this.#renderItems?.find(i => i[keyProperty] === key);
        if (!item) {
            return;
        }
        callback(item);
    }
    handleClick(e) {
        const { selection } = this.options;
        const row = this.rowFromEvent(e);
        if (!row) {
            return;
        }
        if (selection === 'single') {
            this.handleSingleSelection(row.dataset.key);
        }
        else if (selection === 'multi') {
            this.handleMultiSelection(row.dataset.key);
        }
        const table = this.tableFromEvent(e);
        this.focused = this.cellPositionFromEvent(e);
        this.focusCell(table, this.focused.row, this.focused.cell);
    }
    handleKeyDown(e) {
        if (e.defaultPrevented) {
            return;
        }
        if (['Space', 'Enter'].includes(e.code)) {
            this.handlePrimaryAction(e);
        }
        else if (e.code === 'ArrowRight') {
            this.moveFocusRight(e);
        }
        else if (e.code === 'ArrowDown') {
            this.moveFocusDown(e);
        }
        else if (e.code === 'ArrowUp') {
            this.moveFocusUp(e);
        }
        else if (e.code === 'ArrowLeft') {
            this.moveFocusLeft(e);
        }
    }
    handleSingleSelection(key) {
        if (this.selected.includes(key)) {
            return;
        }
        this.selected = [key];
        this.dispatchRender();
        this.dispatchSelected(key);
    }
    handleMultiSelection(key) {
        const { selected } = this;
        if (selected.includes(key)) {
            const index = selected.indexOf(key);
            selected.splice(index, 1);
        }
        else {
            selected.push(key);
        }
        this.dispatchRender();
        this.dispatchSelected(selected);
    }
    focusFirstCell(table) {
        const cell = table.querySelector('[role="columnheader"]');
        if (!cell) {
            return;
        }
        cell.setAttribute('tabindex', '0');
        cell.focus();
        table.removeAttribute('tabindex');
        this.focused = { cell: 0, row: 0 };
    }
    focusCell(table, rowIndex, colIndex) {
        const row = table.querySelectorAll('[role="row"]')[rowIndex];
        if (!row) {
            return false;
        }
        const cell = row.querySelectorAll(`.data-table-cell`)[colIndex];
        if (!cell) {
            return false;
        }
        const current = table.querySelector(`.data-table-cell[tabindex]`);
        cell.setAttribute('tabindex', '0');
        cell.focus();
        if (current) {
            current.removeAttribute('tabindex');
        }
        if (table.hasAttribute('tabindex')) {
            table.removeAttribute('tabindex');
        }
        return true;
    }
    moveFocusRight(e) {
        e.preventDefault();
        const table = this.tableFromEvent(e);
        if (!table) {
            return;
        }
        const { focused } = this;
        if (!focused) {
            this.focusFirstCell(table);
            return;
        }
        if (this.focusCell(table, focused.row, focused.cell + 1)) {
            focused.cell += 1;
        }
    }
    moveFocusLeft(e) {
        e.preventDefault();
        const table = this.tableFromEvent(e);
        if (!table) {
            return;
        }
        const { focused } = this;
        if (!focused) {
            this.focusFirstCell(table);
            return;
        }
        if (this.focusCell(table, focused.row, focused.cell - 1)) {
            focused.cell -= 1;
        }
    }
    moveFocusDown(e) {
        e.preventDefault();
        const table = this.tableFromEvent(e);
        if (!table) {
            return;
        }
        const { focused } = this;
        if (!focused) {
            this.focusFirstCell(table);
            return;
        }
        if (this.focusCell(table, focused.row + 1, focused.cell)) {
            focused.row += 1;
        }
    }
    moveFocusUp(e) {
        e.preventDefault();
        const table = this.tableFromEvent(e);
        if (!table) {
            return;
        }
        const { focused } = this;
        if (!focused) {
            this.focusFirstCell(table);
            return;
        }
        if (this.focusCell(table, focused.row - 1, focused.cell)) {
            focused.row -= 1;
        }
    }
    handlePrimaryAction(e) {
        e.preventDefault();
        const info = this.cellRowFromEvent(e);
        if (!info) {
            return;
        }
        const { row, column } = info;
        // first check if the cell has an focusable element. If so trigger action on it.
        const focusable = column.querySelector('[tabindex="0"]');
        if (focusable) {
            focusable.click();
            return;
        }
        const { selection } = this.options;
        if (selection === 'single') {
            this.handleSingleSelection(row.dataset.key);
        }
        else if (selection === 'multi') {
            this.handleMultiSelection(row.dataset.key);
        }
    }
    handleListScroll(e) {
        const list = e.target;
        const { scrollTop, offsetHeight, scrollHeight } = list;
        const bottom = scrollTop + offsetHeight >= scrollHeight - 20; // 20 is the offset which qualifies as the end. An arbitrary number.
        if (bottom) {
            this.dispatchScrollEnd();
        }
    }
    renderHeader() {
        const cells = this.dispatchHeader();
        return html `
    <div role="rowgroup">
      <div role="row" class="data-table-row data-table-header">
        ${cells}
      </div>
    </div>
    `;
    }
    renderBody() {
        const { active = false } = this.options;
        const items = this.#renderItems;
        const contents = !!items && !!items.length ? items.map(item => this.renderItem(item)) : this.renderEmpty();
        const dbListener = active ? this.dblclickHandler : undefined;
        return html `
    <div class="data-table-body" role="rowgroup" @dblclick="${dbListener}" @scroll="${{ handleEvent: this.handleListScroll, passive: true }}">
      ${contents}
    </div>`;
    }
    renderItem(item) {
        const cells = this.dispatchItem(item);
        const { selected, options } = this;
        const typed = item;
        const key = typed[options.key || 'key'];
        let isSelected = false;
        if (options.selection === 'single') {
            const [selectedKey] = selected;
            if (selectedKey === key) {
                isSelected = true;
            }
        }
        else if (options.selection === 'multi') {
            isSelected = selected.includes(key);
        }
        const classes = {
            'data-table-row': true,
            selected: isSelected,
        };
        return html `
    <div role="row" class="${classMap(classes)}" data-key="${key}" aria-selected="${isSelected ? 'true' : 'false'}">
      <div class="data-table-row-body" role="presentation">
        ${cells}
      </div>
    </div>
    `;
    }
    renderEmpty() {
        const content = this.dispatchEmpty();
        return html `
    <div role="row" class="data-table-row data-table-empty-row">
      <div role="cell">
      ${content}
      </div>
    </div>
    `;
    }
    dispatchHeader() {
        const callback = this.#listeners.header;
        if (!callback) {
            return undefined;
        }
        let content;
        try {
            content = callback();
        }
        catch (e) {
            // eslint-disable-next-line no-console
            console.warn(e);
        }
        return content;
    }
    dispatchItem(item) {
        const callback = this.#listeners.item;
        if (!callback) {
            return undefined;
        }
        let content;
        try {
            content = callback(item);
        }
        catch (e) {
            // eslint-disable-next-line no-console
            console.warn(e);
        }
        return content;
    }
    dispatchEmpty() {
        const callback = this.#listeners.empty;
        if (!callback) {
            return undefined;
        }
        let content;
        try {
            content = callback();
        }
        catch (e) {
            // eslint-disable-next-line no-console
            console.warn(e);
        }
        return content;
    }
    dispatchRender() {
        const callback = this.#listeners.render;
        if (!callback) {
            return;
        }
        try {
            callback();
        }
        catch (e) {
            // eslint-disable-next-line no-console
            console.warn(e);
        }
    }
    dispatchSelected(selection) {
        const callback = this.#listeners.select;
        if (!callback) {
            return;
        }
        try {
            callback(selection);
        }
        catch (e) {
            // eslint-disable-next-line no-console
            console.warn(e);
        }
    }
    dispatchScrollEnd() {
        const callback = this.#listeners.scrollend;
        if (!callback) {
            return;
        }
        try {
            callback();
        }
        catch (e) {
            // eslint-disable-next-line no-console
            console.warn(e);
        }
    }
}
//# sourceMappingURL=DataTable.js.map