<!--
    #VUE3 Todo


The grid will be created to contain cells (like a spreadsheet). A space represents an area that
spans a certain number of cells and can contain a certain number of cards. Keeping all of this
straight can get a little confusing because the terms are highly related but different, so here is
the breakdown in some detail:

## Cell
A single unit in the grid that spans 1 row and 1 column.

## Space
An area that spans a certain number of cells and contains a certain number of cards. As implemented
spaces can only span columns (not rows). We support three kinds of spaces:

- **default** space ('--') This is a normal looking card. It contains 1 card and is 1x1.
- **stacked** space ('==') This space is also (1x1) but contains two cards stacked vertically.
- **wide** space ('-----') This space is 2x1 cells and contains one card.

You might be wondering why we don't just make space and cell the same thing and the largest cards
would be 2x2 instead of 1x2. It has to do with the way we're using css grid for layout. Things are a
lot cleaner if our max card height is a single row in the grid. It also makes it so that the
notation matches the rows array which keeps things easier to understand.`

## Card
Cards are areas within a space. Default and wide spaces contain a single card. Stacked spaces
contain two cards.

## Space Notation
In order to simplify the definition of spaces, they can be represented with the notation above and
converted to and from objects with the functions below.

## Templates

You can define a template using space notation like this:

    '== -- -----',
    '-- == -- --',
    '----- -- ==',
    '-- -- == --',


## Basic Example

    <px-card-grid
        :items="fruits"
        :gap="30"
        :maxColumns="4"
        :debug="true",
        :templates="{
            4: ['== -- -----', '-- == -- --', '----- -- ==', '-- -- == --'],
            3: ['== -- --', '-- -- ==', '-- == --'],
            2: ['-- --', '-----', '== --'],
        }"
    >
        <template #default="{ item }">{{ item.name }}</template>
    </px-card-grid>


## Custom Item Selection

    By default items will be placed in the template in sequential order but it's possible to take
    full control of which item is placed in which slot but providing a callback with the getItemFn
    property. This function will receive an object with parameters describing the space and expects
    an item to be returned. The function will be called once for the number of spaces in the layout
    (include for orphans).

    When using this method it isn't even necessary to set the items property but you also need to
    provide the getItemCountFn callback. That should be the number of items without orphans (you will
    still get the callback for orphans).

    <px-card-grid
        :gap="30"
        :maxColumns="4"
        :debug="true",
        :templates="{
            4: ['== -- -----', '-- == -- --', '----- -- ==', '-- -- == --'],
            3: ['== -- --', '-- -- ==', '-- == --'],
            2: ['-- --', '-----', '== --'],
        }"
        :get-item-fn="getItem"
        :get-item-count="getItemCount"
    >
        <template #default="{ item }">{{ item.name }}</template>
    </px-card-grid>

    getItem(space) {
        if (space.type == 'stacked') {
            ...
        }
        else {
            ...
        }
    }

    getItemCount() {
        return ...;
    }


-->

<template>
    <div :class="bem({ debug: debug })">
        <div :class="bem('grid', { cols: String(columnCount) })" :style="gridStyle">
            <div :class="bem('row')" v-for="(row, rowIndex) in rows" :key="rowIndex">
                <div
                    :class="bem('space', { layout: space.name })"
                    v-for="(space, spaceIndex) in row"
                    :key="spaceIndex"
                >
                    <div
                        v-for="item in space.cards"
                        :key="getItemKey(item)"
                        :class="bem('card')"
                        :ref="cardRefFromItem(item)"
                    >
                        <div v-if="item.id" :id="item.id" :class="bem('scroll-id')">
                            {{ item.id }}
                        </div>
                        <slot
                            name="default"
                            :index="items.indexOf(item)"
                            :item="item || {}"
                            :size="space.name"
                        >
                            {{ items.indexOf(item) }}:{{ space.name }}
                        </slot>
                    </div>
                </div>
            </div>
        </div>
    </div>
</template>

<script>
import aspect from '../plugins/filters/aspect.js';
import PxBaseItems from '../bases/PxBaseItems.vue';
import PxBaseResize from '../bases/PxBaseResize.vue';

// prettier-ignore
const defaultTemplates = {
    5: [
        '== -- == -- ==',
        '-- == -- == --'
    ],
    4:[
        '== -- -----',
        '-- == -- --',
        '----- -- ==',
        '-- -- == --',
    ],
    3: [
        '== -- --',
        '-- -- ==',
        '-- == --',
    ],
    2: [
        '-- --',
        '-- ==',
        '== --',
    ]

};

// prettier-ignore
const spaces = [
     { name: 'default', notation: '--',    cards: 1, colspan: 1, },
     { name: 'wide',    notation: '-----', cards: 1, colspan: 2, },
     { name: 'stacked', notation: '==',    cards: 2, colspan: 1  },
];

const spaceNotationToObject = symbol => {
    return spaces.find(s => s.notation == symbol);
};

// converts a template in space notation into an array of rows containing space objects
const spaceTemplateToSpaceRows = rows => {
    let spaces = [];
    rows.forEach(row => {
        row = row.trim().split(/[\s\r\n]+/);
        spaces.push(row.map(symbol => spaceNotationToObject(symbol)));
    });
    return spaces;
};

const cardCountInRowTemplate = rowTemplate => {
    let count = rowTemplate.reduce((total, space) => {
        return total + space.cards;
    }, 0);
    return count;
};

export default {
    name: 'px-card-grid',
    mixins: [PxBaseResize, PxBaseItems],
    props: {
        templates: { type: Object, default: () => defaultTemplates },
        maxColumns: { type: Number, default: 6 },
        maxRows: { type: Number, default: null },
        allowOrphans: { type: Boolean, default: true },
        padOrphans: { type: Boolean, default: false },
        cardMinWidth: { type: Number, default: 240 },
        cardMaxWidth: { type: Number, default: 320 },
        cardAspect: { default: 3 / 4 },
        gap: { type: Number, default: 24 },
        debug: { type: Boolean, default: false },
        getItemCountFn: { default: null },
        getItemFn: { default: null },
        centerSingleRow: { type: Boolean, default: true },
    },

    data() {
        return {
            columnCount: 0,
            rowTemplates: null,
        };
    },
    computed: {
        gridStyle() {
            let rowCount = this.rows?.length || 0;
            let colCount = this.columnCount;

            if (this.centerSingleRow && rowCount == 1) {
                colCount = 0;
                this.rows[0].forEach(sp => {
                    colCount += sp.name == 'wide' ? 2 : 1;
                });
            }

            const defAspect = aspect(this.cardAspect);
            const wideAspect = (this.cardMinWidth * 2 + this.gap) / (this.cardMinWidth / defAspect);
            const maxGridWidth = this.cardMaxWidth * colCount + this.gap * (this.columnCount - 1);

            return {
                // prettier-ignore
                gridTemplateColumns: `repeat(${colCount}, minmax(0, ${1 / colCount}fr))`,
                maxWidth: this.cssPx(maxGridWidth),
                '--gap': this.cssPx(this.gap),
                '--aspect': defAspect,
                '--aspect-wide': wideAspect,
            };
        },

        rows() {
            // Returns an array of rows, each of which contains an array of spaces each
            // of which contains 1 or more items that can be used to create cards.

            let itemCount = this.getItemCount();

            if (!this.rowTemplates || !itemCount || this.columnCount == 0) {
                return [];
            }

            // get relevant row templates based on the number of columns
            let templates = this.rowTemplates[this.columnCount];

            if (!templates) {
                console.error(`Could not create grid for ${this.columnCount} columns.`);
                return [];
            }

            let rows = [];
            let rowIndex = 0;
            let itemIndex = 0;

            while (itemIndex < itemCount && rowIndex < 1000) {
                let template = templates[rowIndex % templates.length];
                let rowCardCount = cardCountInRowTemplate(template);
                let isOrphanRow = itemIndex + rowCardCount > itemCount;

                // render if this is not an orphan row or it is and we allow it
                if (!isOrphanRow || this.allowOrphans) {
                    let row = template.map(spaceTemplate => {
                        let cards = Array(spaceTemplate.cards).fill(0);

                        cards = cards.map(() => {
                            let item = null;
                            const isOrphanPadding = itemIndex >= itemCount;
                            const isOrphan = isOrphanRow && !isOrphanPadding;

                            if (!isOrphanPadding || this.padOrphans) {
                                item = this.getItemForSpace({
                                    index: itemIndex,
                                    type: spaceTemplate.name,
                                    orphan: isOrphan,
                                    orphanPadding: isOrphanPadding,
                                });
                            }

                            itemIndex++;
                            return item;
                        });

                        return {
                            name: spaceTemplate.name,
                            cardCount: spaceTemplate.cards,
                            cards: cards,
                        };
                    });

                    rows.push(row);
                }

                // get out of the loop
                else {
                    itemIndex += rowCardCount;
                }

                rowIndex++;
            }

            if (rows?.length && !this.padOrphans && itemIndex > itemCount) {
                // remove empty
                rows[rows.length - 1] = rows[rows.length - 1].filter(sp => {
                    sp.cards = sp.cards.filter(card => !!card);
                    return sp.cards.length;
                });
            }

            return rows;
        },
    },
    watch: {
        templates() {
            this.updateRowTemplates();
        },
    },
    mounted() {
        this.updateRowTemplates();
    },
    beforeUnmount() {
        this.resizeObserver.disconnect();
        delete this.resizeObserver;
    },
    methods: {
        getItemCount() {
            if (this.getItemCountFn) {
                return this.getItemCountFn();
            }

            return this.items?.length || 0;
        },

        getItemForSpace(space) {
            if (this.getItemFn) {
                return this.getItemFn(space);
            }

            if (space.orphanPadding) {
                return space;
            }

            const index = space.index;

            if (this.items && index < this.items.length) {
                return this.items[index];
            }

            console.warn(`No item found for space at index ${index}.`);
            return null;
        },

        updateRowTemplates() {
            // get a single card (needed to fill missing templates)
            const single = spaceNotationToObject('--');

            let rowTemplates = {};

            for (let i = this.maxColumns; i > 0; i--) {
                // if we have a template for this count, parse it and use it
                if (i in this.templates) {
                    rowTemplates[i] = spaceTemplateToSpaceRows(this.templates[i]);
                }

                // otherwise create one with all singles
                else {
                    rowTemplates[i] = [Array(i).fill(single)];
                }
            }

            this.rowTemplates = rowTemplates;
        },

        resize() {
            let count = 1;
            let availableWidth = this.$el.offsetWidth;

            if (!availableWidth || this.availableWidth == availableWidth) {
                return;
            }

            this.availableWidth = availableWidth;

            for (let i = this.maxColumns; i > 0; i--) {
                let neededWidth = this.cardMinWidth * i + this.gap * (i - 1);
                if (neededWidth <= availableWidth) {
                    count = i;
                    break;
                }
            }
            this.columnCount = count;
        },

        cardRefFromItem(item) {
            if (!item) {
                return null;
            }

            const key = this.getItemKey(item);

            if (key && key.toString) {
                const ref = `card-${key.toString()}`;
                return ref;
            }

            return null;
        },

        cardElementFromItem(item) {
            const ref = this.cardRefFromItem(item);
            if (ref && this.$refs[ref] && this.$refs[ref].length) {
                return this.$refs[ref][0];
            }
            return null;
        },
    },
};
</script>

<style lang="scss">
@use '../styles/px.scss' as *;

.px-card-grid {
    width: 100%;
    max-width: 100%;

    &__grid {
        display: grid;
        grid-gap: var(--gap);
        justify-content: center;
        margin: 0 auto;
    }

    &__row {
        display: contents;
    }

    &__space {
        position: relative;
        display: flex;
        flex-direction: column;
        justify-content: stretch;
        align-items: stretch;
        grid-gap: var(--gap);
        gap: var(--gap);

        &--layout-stacked {
            grid-column: span 1 / auto;
            aspect-ratio: var(--aspect);
            // prettier-ignore
            @include grid-art((
                '100%   | ',
                'top    | #(0, 1fr)',
                'bottom | #(0, 1fr)'
            ));
        }

        &--layout-default {
            grid-column: span 1;
            aspect-ratio: var(--aspect);
            @include grid-art(('100%   | ', 'card   | #(0, 1fr)'));
        }
        &--layout-wide {
            grid-column: span 2;
            //aspect-ratio: var(--aspect-wide);

            @include grid-art(('100%   | ', 'card   | #(0, 1fr)'));
        }
    }

    &__card {
        display: grid;
        align-items: stretch;
        justify-content: stretch;
        position: relative;
        flex: 1;
        grid-template-columns: 100%;
        grid-template-rows: auto;
        > * {
            width: 100%;
        }
    }

    // special case : wide columns in two columns need an aspect ration
    &__grid--cols-2 & {
        &__space--layout-wide {
            aspect-ratio: var(--aspect-wide);
        }
    }

    &--debug & {
        &__card {
            @include after() {
                @include abs;
                background-color: rgba(red, 0.05);
                border-radius: 5px;
                border: 2px dashed rgba(red, 0.2);
                pointer-events: none;
            }
        }
    }

    // element with id set to the section id, placed just outside the top of the section to help
    // with scroll position
    &__scroll-id {
        position: absolute;
        width: 1px;
        height: 1px;
        top: -80px;
        opacity: 0;
    }
}
</style>
