<!--

    // column definitions
    {
        // (null) an optional unique id for the column
        id,

        // (null) the column label (defaults to the property name if not specified)
        label,

        // (null) a slot template name used to define the rendering for the column header label
        headerTemplate,

        // (auto) size of the column, specified using grid template syntax
        size,

        // (null) a boolean or function that determines whether the column supports sorting, if null the column will be sortable if a value for property or sortValue is also defined
        sortable,

        // (null) the property that will be displayed in the column (if undefined, the column needs to be rendered using a slot)
        property,

        // (null) function that returns the value associated with the column for a given item (overrides property for display and sorting)
        value: (item, column) => {
            // return the value to sort by
        },

        // (null) a slot template name used to define the rendering for the column (required if property is unset)
        template,

        // (hidden | wrap | ellipsis) whether to wrap or show ellipsis (when using the default cell template)
        overflow,

        // (start | left | center | right | end) text alignment for the column (when using the default cell template)
        align,

        // (null) class that will be applied to cells in this column
        class
    }

-->
<template>
    <div :class="bem()">
        <div
            ref="table"
            :class="[bem('table')]"
            :style="{
                gridTemplateColumns: columnWidths,
                gridAutoRows: rowSize,
            }"
        >
            <!-- header -->

            <div
                :class="[
                    bem('row-header', { fixed: fixedHeader }),
                    !!rowClass ? `${rowClass} ${rowClass}--header` : '',
                ]"
                :style="{ gridColumn: `1 / ${columnsCalc.length + 1}` }"
            >
                <component
                    :is="isColumnSortable(c) ? 'button' : 'div'"
                    :class="[
                        bem('cell-header', {
                            sortable: isColumnSortable(c),
                            sorted: isColumnSorted(index),
                        }),
                    ]"
                    v-for="(c, index) in columnsCalc"
                    :key="index"
                    :role="isColumnSortable(c) ? 'button' : null"
                    @click="isColumnSortable(c) ? onSortClick(c, index) : null"
                >
                    <slot v-if="c.headerTemplate" :name="c.headerTemplate" v-bind:column="c"></slot>

                    <span
                        v-else
                        :class="
                            bem('cell-header-default', { overflow: c.overflow, align: c.align })
                        "
                    >
                        {{ c.label || c.property }}
                    </span>

                    <px-icon
                        v-if="isColumnSortable(c)"
                        v-show="
                            !c.align ||
                            c.align == 'start' ||
                            c.align == 'left' ||
                            isColumnSorted(index)
                        "
                        :class="bem('sort-icon')"
                        :src="sortAscending ? iconSortedAscending : iconSortedDescending"
                        :style="{ opacity: isColumnSorted(index) ? 1 : 0 }"
                    />
                </component>
            </div>

            <!-- rows -->

            <div
                :class="[bem('row'), rowClass]"
                v-for="(item, itemIndex) in items"
                :key="getItemKey(item) + key"
                :style="{ gridColumn: `1 / ${columnsCalc.length + 1}` }"
            >
                <div
                    :class="[bem('cell'), cellClass, c.class]"
                    v-for="(c, index) in columnsCalc"
                    :key="index"
                >
                    <!-- drag-handle -->
                    <slot v-if="c.template == 'drag-col'">
                        <div :class="bem('drag-handle')">
                            <px-icon :src="dragHandleIcon" />
                        </div>
                    </slot>

                    <!-- expand-col -->
                    <slot v-if="c.template == 'expand-col'">
                        <button :class="bem('expand-toggle')" @click="toggleExpandRow(item)">
                            <px-icon
                                :src="
                                    isRowExpanded(item) ? expandIconExpanded : expandIconUnexpanded
                                "
                            />
                        </button>
                    </slot>

                    <!-- value template -->
                    <slot
                        v-else-if="
                            (c.template && $slots[c.template]) || (c.property && $slots[c.property])
                        "
                        :name="c.template || c.property"
                        :item="item"
                        :index="itemIndex"
                    ></slot>

                    <!-- string (default) -->
                    <span
                        v-else
                        :class="bem('cell-default', { overflow: c.overflow, align: c.align })"
                    >
                        {{ valueToString(getItemValue(item, c)) }}
                    </span>
                </div>

                <PxTransitionExpand>
                    <div
                        v-if="expandEnabled && expandedItemKeys.includes(getItemKey(item))"
                        :class="[bem('row-expand'), expandClass]"
                        :style="{ gridColumn: `1 / ${columnsCalc.length + 1}` }"
                    >
                        <slot name="expand" :item="item" :index="itemIndex"></slot>
                    </div>
                </PxTransitionExpand>
            </div>
        </div>
    </div>
</template>

<script>
import PxBaseItems from '../bases/PxBaseItems.vue';
import PxIcon from './PxIcon.vue';
import PxTransitionExpand from './PxTransitionExpand.vue';

// TODO: need to find feather replacement icons
import iconSortedAsc from '../assets/feather/chevron-down.svg?url';
import iconSortedDesc from '../assets/feather/chevron-up.svg?url';
import iconDrag from '../assets/icons/drag.svg?url';
import iconExpanded from '../assets/feather/minus-square.svg?url';
import iconUnexpanded from '../assets/feather/plus-square.svg?url';

import Sortable from 'sortablejs';
import { arrayMoveItem } from '../utils/utils.js';

export default {
    name: 'px-table',
    mixins: [PxBaseItems],
    components: { PxIcon, PxTransitionExpand },
    props: {
        /**
         * This determines whether the underlying array that contains the items will be directly
         * updates when items are moved, removed or sorted. If false, the parent component needs to
         * listen to the `update` event and make changes to the underlying data.
         */
        autoUpdateItems: { type: Boolean, default: true },

        /** Class that gets applied to individual cells */
        cellClass: { default: null },

        /** Can be an array of objects (see above) or strings (just property names) */
        columns: { type: Array, default: null },

        /** defines the sizes of rows ( grid-template-row syntax) */
        rowSize: { type: String, default: 'auto' },

        /**
         * A class that gets set on each of the generated rows. Note that the header will also have
         * this applied along with the modified class `${rowClass}--header`.
         */
        rowClass: { default: '' },

        // whether to the header row is fixed (sticky)
        fixedHeader: { type: Boolean, default: true },

        // sorting

        /**
         * Enables sorting on the column (PxTable tracks which column is current selected for
         * sorting and a direction (ascending/descending) and raises and event (@sort) when it
         * changes, but the actual sorting need to be handle externally by sorting the items).
         */
        sortEnabled: { type: Boolean, default: false },

        /**
         * A function that can be used to provide custom sorting. If not provided, a default
         * function will be used that compares the values of the property in the items.
         *
         * The function receives the two items being compared, the column definition and a boolean
         * that will be true if the sort should be in ascending order.
         *
         * `function sort(itemA, itemB, column, isAscending) { ... }`
         *
         */
        sortFn: { type: Function, default: null },

        iconSortedAscending: { type: String, default: iconSortedAsc },
        iconSortedDescending: { type: String, default: iconSortedDesc },

        // dragging
        dragEnabled: { type: Boolean, default: false },

        /**
         * The placement of the drag handle (if enabled). Can be 'start' or 'end' to place the drag
         * at the beginning or end of the column. Set to null to disable.
         */
        dragHandlePlacement: { type: String, default: 'start' },
        dragHandleIcon: { type: String, default: iconDrag },

        // other actions
        removeEnabled: { type: Boolean, default: false },

        expandEnabled: { type: Boolean, default: true },
        expandClass: { default: null },

        /**
         * If true, only a single item can be expanded at a time. Set to false to allow multiple
         * items to be simultaneously expanded.
         */
        expandToggles: { type: Boolean, default: true },
        expandIconExpanded: { type: String, default: iconExpanded },
        expandIconUnexpanded: { type: String, default: iconUnexpanded },

        // can be used to temporarily disable all item level actions (expand, drag, remove, etc.)
        disabled: { type: Boolean, default: false },

        /**
         * For maximum compatibility, you can disable the use of subgrid on table rows. This feature
         * is [available in the latest version of all major
         * browsers](https://developer.mozilla.org/docs/Web/CSS/CSS_grid_layout/Subgrid) since 2023
         * but if you need to support older browsers, you can disable it here. If disabled, we'll
         * set "display:contents" on the generated table rows which will disable box model
         * functionality (backgrounds, interactivity, etc.) on the rows themselves but the cells
         * will be unaffected and the layout will still work.
         *
         * Note that default styling for borders, etc. also relies on subgrid so you may need to
         * provide altnerate cell styles (with bottom borders, etc.) if you disable this. That can
         * be done with the cellClass prop.
         */
        disableSubGrid: { type: Boolean, default: false },
    },
    data() {
        return {
            key: 1,
            sortColumnIndex: null,
            sortAscending: false,
            expandedItemKeys: [],
        };
    },
    computed: {
        columnsCalc() {
            const colsDefault = this.items?.length ? Object.keys(this.items[0]) : [];

            const cols = (this.columns || colsDefault || []).map(c => {
                if (typeof c == 'string') {
                    return {
                        property: c,
                        label: c,
                        sortable: true,
                    };
                }
                return { ...c };
            });

            const actionClass = this.bem('cell', { action: true });

            // drag handle

            if (this.dragEnabled && this.dragHandlePlacement == 'start') {
                cols.unshift({
                    template: 'drag-col',
                    size: 'min-content',
                    class: actionClass,
                });
            }

            if (this.dragEnabled && this.dragHandlePlacement == 'end') {
                cols.push({
                    template: 'drag-col',
                    size: 'min-content',
                    class: actionClass,
                });
            }

            // expand button

            if (this.expandEnabled && !!this.$slots.expand) {
                cols.unshift({ template: 'expand-col', size: 'min-content', class: actionClass });
            }

            return cols;
        },
        columnWidths() {
            return this.columnsCalc.map(col => col.size || 'auto').join(' ');
        },
    },
    // watch: {},
    mounted() {
        if (this.dragEnabled) {
            this.sortable = Sortable.create(this.$refs.table, {
                draggable: `.${this.bem('row')}`,
                filter: `.${this.bem('row-expand')}, .${this.bem('row-header')}`,
                chosenClass: this.bem('row--chosen'),
                ghostClass: this.bem('row--ghost'),
                dragClass: this.bem('row--drag'),
                onEnd: this.onDragEnd,
            });
        }
    },
    beforeUnmount() {
        if (this.sortable) {
            this.sortable.destroy();
        }
    },
    methods: {
        getItemValue(item, column) {
            if (column.value) {
                return column.value(item, column);
            }
            if (column.property) {
                return item[column.property];
            }
            return null;
        },

        valueToString(value) {
            if (value === null || value === undefined) {
                return '';
            }

            if (typeof value == 'object' && typeof value.getMonth === 'function') {
                return value.toLocaleDateString();
            }
            if (typeof value == 'boolean') {
                return value ? 'True' : 'False';
            }
            return String(value);
        },

        // expand

        isRowExpanded(item) {
            return this.expandedItemKeys.includes(this.getItemKey(item));
        },

        expandRow(item) {
            if (this.expandToggles) {
                this.expandedItemKeys = [this.getItemKey(item)];
            } else {
                this.expandedItemKeys = [...this.expandedItemKeys, this.getItemKey(item)];
            }
        },

        unexpandRow(item) {
            if (this.expandToggles) {
                this.expandedItemKeys = [];
            } else {
                this.expandedItemKeys = this.expandedItemKeys.filter(
                    key => key !== this.getItemKey(item),
                );
            }
        },

        toggleExpandRow(item) {
            if (this.isRowExpanded(item)) {
                this.unexpandRow(item);
            } else {
                this.expandRow(item);
            }
        },

        // sorting

        isColumnSortable(column) {
            if (!this.sortEnabled || column.sortable === false) {
                return false;
            }

            return !!(column.property || column.value || column.sortable === true || !!this.sortFn);
        },

        isColumnSorted(columnIndex) {
            return this.sortColumnIndex == columnIndex;
        },

        sortDefault(itemA, itemB, column, isAscending) {
            const aValue = this.getItemValue(itemA, column);
            const bValue = this.getItemValue(itemB, column);

            if (aValue < bValue) {
                return isAscending ? -1 : 1;
            }
            if (aValue > bValue) {
                return isAscending ? 1 : -1;
            }
            return 0;
        },

        onSortClick(column, columnIndex) {
            if (this.sortColumnIndex == columnIndex) {
                this.sortAscending = !this.sortAscending;
            } else {
                this.sortAscending = true;
            }

            this.sortColumnIndex = columnIndex;

            if (this.autoUpdateItems) {
                const sortFn = this.sortFn || this.sortDefault;

                this.items.sort((a, b) => {
                    return sortFn(a, b, column, this.sortAscending);
                });
            }
        },

        // drag

        onDragEnd(e) {
            const fromIndex = e.oldDraggableIndex;
            const toIndex = e.newDraggableIndex;

            if (fromIndex == toIndex) {
                return;
            }

            this.sortColumnIndex = null;

            if (this.autoUpdateItems && this.items?.length) {
                arrayMoveItem(this.items, fromIndex, toIndex);
            }

            this.key++;
        },
    },
};
</script>

<style lang="scss">
@use '../styles/px.scss' as *;
@use '@thinkpixellab-public/px-styles/src/modules/controls' as *;

@at-root {
    :root {
        // background for the entire table
        --px-table-bg: #{clr(page-bg)};

        // background for the entire table
        --px-table-fg: #{clr(page-fg)};

        // outer border for the entire table
        --px-table-border: 1px solid #{gray(2, 0.5)};

        // outer border radius for the entire table
        --px-table-radius: 0.5em;

        // inner border radius (may need to be less than outer in some cases)
        --px-table-inner-radius: initial;

        // background for the header row
        --px-table-header-bg: #{gray(8)};

        // background for the header row
        --px-table-header-fg: unset;

        // separator border below the header row
        --px-table-header-border: 1px solid #{gray(2, 0.5)};

        // background for all non-header rows
        --px-table-row-bg: unset;

        // background for all non-header rows
        --px-table-row-fg: unset;

        // background for odd non-header rows
        --px-table-row-odd-bg: unset;

        // background for odd non-header rows
        --px-table-row-odd-fg: unset;

        // separator border below each row
        --px-table-row-border: 1px solid #{gray(2, 0.5)};

        // cell padding
        --px-table-cell-padding: 1em;

        // header cell padding
        --px-table-header-cell-padding: 1em;

        // header cell hover (when clickable)
        --px-table-header-cell-hover-bg: #{gray(5)};
        --px-table-header-cell-hover-fg: unset;

        // header cell selected (e.g. sorted)
        --px-table-header-cell-selected-bg: #{accent(0)};
        --px-table-header-cell-selected-fg: #{contrast-color(accent(0))};
    }
}

.px-table {
    // row states that can be used when defining config for cell and header

    $cell-state-aliases: (
        last-row: '.px-table__row:last-child &',
        even-row: '.px-table__row:nth-child(even) &',
        odd-row: '.px-table__row:nth-child(odd) &',
        sortable: '&.px-table__cell--sortable',
        sorted: '&.px-table__cell--sorted',
    );

    $cell: flat-merge(
        get(
            'controls.table.cell',
            (
                padding: 0.5em,
                last-row: (
                    border-bottom: none,
                ),
            )
        )
    );

    background: var(--px-table-bg);
    color: var(--px-table-fg);
    border: var(--px-table-border);
    border-radius: var(--px-table-radius);

    &__table {
        position: relative;
        display: grid;
        height: 100%;
        max-height: 100%;
        border-radius: inherit;
        box-sizing: border-box;
    }

    &__row-header,
    &__row {
        position: relative;
        display: grid;
        grid-template-columns: subgrid;
    }

    &__row-header {
        //@include css-map($header, $aliases: $cell-state-aliases);
        z-index: 1;
        background: var(--px-table-header-bg);
        color: var(--px-table-header-fg);
        border-bottom: var(--px-table-header-border);

        border-top-left-radius: var(--px-table-inner-radius, inherit);
        border-top-right-radius: var(--px-table-inner-radius, inherit);

        &[role='button'] {
            cursor: pointer;
        }
        &--fixed {
            position: sticky;
            top: 0;
        }
    }

    &__row {
        background: var(--px-table-row-bg);
        color: var(--px-table-row-fg);

        &:nth-child(odd) {
            background: var(--px-table-row-odd-bg);
            color: var(--px-table-row-odd-fg);
        }

        &:not(:last-child) {
            border-bottom: var(--px-table-row-border);
        }

        &:last-child {
            border-bottom-left-radius: var(--px-table-inner-radius, inherit);
            border-bottom-right-radius: var(--px-table-inner-radius, inherit);
        }
    }

    &__cell-header {
        @include control-reset-hard();
        &:first-child {
            border-top-left-radius: var(--px-table-inner-radius, inherit);
        }

        &:last-child {
            border-top-right-radius: var(--px-table-inner-radius, inherit);
        }
    }

    &__cell-header,
    &__cell {
        position: relative;
        width: 100%;
        height: 100%;
        box-sizing: border-box;
        display: flex;
        align-items: center;
        padding: var(--px-table-cell-padding);

        // overflow
        overflow: hidden;
        white-space: nowrap;
        text-overflow: ellipsis;
        line-height: 1.33;

        &--action {
            padding: 0.5em;
        }
    }

    &__cell-header {
    }

    &__cell-header-default,
    &__cell-default {
        display: block;
        max-width: 100%;
        width: 100%;
        overflow: hidden;

        // overflow

        text-overflow: ellipsis;

        &--overflow-ellipsis {
            text-overflow: ellipsis;
        }

        &--overflow-wrap {
            white-space: normal;
        }

        // align

        text-align: start;

        &--align-left,
        &--align-start {
            text-align: start;
        }

        &--align-center {
            text-align: center;
        }

        &--align-right,
        &--align-end {
            text-align: end;
        }
    }

    &__cell-header {
        // padding-top: 0.75em;
        // padding-bottom: 0.75em;

        padding: var(--px-table-header-cell-padding);

        &--sortable {
            cursor: pointer;
            &:hover {
                background: var(--px-table-header-cell-hover-bg, inherit);
                color: var(--px-table-header-cell-hover-fg, inherit);
            }
        }

        &--sorted,
        &--sorted:hover {
            cursor: pointer;
            background: var(--px-table-header-cell-selected-bg, inherit);
            color: var(--px-table-header-cell-selected-fg, inherit);
        }
    }

    &__expand-toggle {
        @include control-reset-hard();
        cursor: pointer;
        width: auto;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
        opacity: 0.5;
        @include transition(opacity);
        &:hover {
            opacity: 1;
        }
    }

    &__drag-handle {
        cursor: grab;
        width: 1.5em;
        height: 100%;
        display: flex;
        align-items: center;
        justify-content: center;
        opacity: 0.25;
        @include transition(opacity);
        &:hover {
            opacity: 1;
        }
    }

    &__row--drag {
        border: 1px solid accent();
        background-color: clr(page-bg);
    }

    &__row--ghost {
        opacity: 0.5;
        background-color: gray(8);
    }

    &__sort-icon {
        margin-left: 0.5em;
        flex: none;
    }
}
</style>
