<script lang="ts" setup>
import { ref, computed, onMounted, onBeforeUnmount, watch, provide, reactive, PropType, nextTick } from 'vue';
import {
    getGridGapDefault,
    getColumnCountDefault,
    getWindowMarginDefault,
    getItemRatioHeightDefault,
    debugLog,
} from './utils';
import { throttle } from 'lodash-es';
import PhotoBrowserController from '../../../../Web/src/Pages/Photos/PhotoBrowserController';

interface VirtualGridInterface extends Vue {
    resetGrid: () => void;
}

interface Item<P> {
    id: string;
    injected?: P;
    height: number;
    width?: number;
    columnSpan: number;
    newRow?: boolean;
    renderComponent: Component;
}

interface ContainerData {
    windowSize: ElementSize;
    windowScroll: ElementScroll;
    elementWindowOffset: number;
    elementSize: ElementSize;
}

interface ElementSize {
    width: number;
    height: number;
}

interface ElementScroll {
    x: number;
    y: number;
}

interface ConfigData<P> {
    windowMargin: number;
    gridGap: number;
    columnCount: number;
    entries: Item<P>[];
}

interface Cell<P> extends Item<P> {
    columnNumber: number;
    rowNumber: number;
    offset: number;
}

interface LayoutData<P> {
    totalHeight: number;
    cells: Cell<P>[];
}

interface RenderData<P> {
    cellsToRender: Cell<P>[];
    firstRenderedRowNumber: number | null;
    firstRenderedRowOffset: number | null;
}

const props = defineProps({
    items: { type: Array as () => Item<any>[], required: true },
    updateFunction: { type: Function as () => () => Promise<boolean>, default: () => () => Promise.resolve(true) },
    getGridGap: { type: Function, default: getGridGapDefault },
    getColumnCount: { type: Function, default: getColumnCountDefault },
    getWindowMargin: { type: Function, default: getWindowMarginDefault },
    getItemRatioHeight: { type: Function, default: getItemRatioHeightDefault },
    scrollElement: { type: HTMLDivElement, default: null },
    updateTriggerMargin: { type: Number, default: 700 },
    loader: { type: Object as PropType<Component>, default: null },
    debug: { type: Boolean, default: false },
    itemComponent: { type: Object as PropType<Component>, required: true },
    headerComponent: { type: Object as PropType<Component>, required: true },
    photoBrowserController: { type: Object as PropType<PhotoBrowserController>, required: true }
});

const rowOffsets = ref([]);
const updateLock = ref(false);
// const itemsDirty = ref(false);
const bottomReached = ref(false);
const virtualGridRef = ref<Element | null>(null);

const windowSize = ref({ height: 0, width: 0 });
const elementWindowOffset = ref(0);
const elementHeight = ref(0);
const elementWidth = ref(0);

const structuredItems = computed<Item<P>[]>(() => computeItemStructure(props.items));
const configData = computed<ConfigData<any>>(() => computeConfigData(windowSize.value, elementWidth.value, structuredItems.value));
const layoutData = computed<LayoutData<any>>(() => computeLayoutData(configData.value));
const renderData = computed<RenderData<any>>(() => computeRenderData(configData.value, elementWindowOffset.value, windowSize.value, layoutData.value));

onMounted(() => {
    virtualGridRef.value = virtualGridRef.value as Element;
    initiliazeGrid();
    window.addEventListener('resize', resize);
    (props.scrollElement ?? window).addEventListener('scroll', scroll);
});

onBeforeUnmount(() => {
    window.removeEventListener('resize', resize);
    (props.scrollElement ?? window).removeEventListener('scroll', scroll);
});

watch(() => props.scrollElement, (newScrollElement, oldScrollElement) => {
    (oldScrollElement ?? window).removeEventListener('scroll', scroll);
    (newScrollElement ?? window).addEventListener('scroll', scroll);
});

watch(structuredItems, (newValue) => {
  console.log('structuredItems.value changed:', newValue);
});

watch(() => elementWidth.value, (newValue) => {
  console.log('elementWidth.value changed:', newValue);
});

watch(() => windowSize.value.width, (newValue) => {
  console.log('windowSize.value.width changed:', newValue);
});

// watch(() => props.items, () => {
//   nextTick(() => {
//     updateContainerSize();
//   });
// }, { deep: true });

const calculateAfterScroll = () => {
  if (virtualGridRef.value) {
    elementWindowOffset.value = getElementOffset(virtualGridRef.value);
  }
};

const calculateAfterResize = () => {
  windowSize.value = getWindowSize();
  const elementSize = getElementSize(virtualGridRef.value);
  elementWidth.value = elementSize.width;
  elementHeight.value = elementSize.height;
};

const calculateAfterNewContent = () => {
  if (virtualGridRef.value) {
    elementHeight.value = (virtualGridRef.value as Element).getBoundingClientRect().height;
  }
};

function computeContainerData(): void {
    if (virtualGridRef.value === null) {
        return;
    }
    calculateAfterResize();
    calculateAfterScroll();
    calculateAfterNewContent();
}

const resize = throttle(() => {
  calculateAfterResize();
  loadMoreData(true);
}, 500, { leading: false, trailing: true });

const scroll = throttle(() => {
  requestAnimationFrame(loadMoreData);
}, 250, { leading: false, trailing: true });

function initiliazeGrid(): void {
    nextTick(async () => {
      loadMoreData(true);
    });
}

function loadMoreData(fromResize = false): void {
    console.log('loadMoreData.');
    if (updateLock.value) return;
    loadMoreDataAsync(fromResize).then();
}

const loadMoreDataAsync = async (fromResize: boolean = false): Promise<void> => {
  if (fromResize) calculateAfterResize();
  calculateAfterScroll();
  calculateAfterNewContent();

  const windowBottom = windowSize.value.height;
  const bottomTrigger = Math.max(
      0,
      elementWindowOffset.value + elementHeight.value - props.updateTriggerMargin
  );
  if (!bottomReached.value && windowBottom >= bottomTrigger && !updateLock.value) {
    updateLock.value = true;
    const isLastBatch = await props.updateFunction();

    if (isLastBatch) {
      bottomReached.value = true;
    }

    updateLock.value = false;
    await loadMoreDataAsync();
  }
  return;
};

function computeItemStructure(items: Item<any>[]): Item<P>[] {
  console.log('computeItemStructure');
  return props.items.reduce((acc, item, index) => {
      const currentGroupKey = item.injected ? [item.injected.DisplayDate, item.injected.Store?.Name] : null;
      
      if (currentGroupKey && (!acc.lastGroupKey || currentGroupKey[0] !== acc.lastGroupKey[0] || currentGroupKey[1] !== acc.lastGroupKey[1])) {
        acc.entries.push({
          id: `${currentGroupKey[0]}-${currentGroupKey[1]}`,
          columnSpan: 0,
          newRow: true,
          height: 50,
          injected: { 
            dateStoreGroupInfo: { 
              date: currentGroupKey[0], 
              store: currentGroupKey[1] 
            } 
          }
        });
      }

      acc.lastGroupKey = currentGroupKey;
      acc.entries.push(item);
      return acc;
    }, { entries: [], lastGroupKey: null }).entries;
}

function computeConfigData(windowSize: ElementSize, elementWidth: number, items: Item<any>[]): ConfigData<any> {
  console.log('computeConfigData');
    if (!elementWidth || items.length === 0) {
      return {
        windowMargin: 0,
        gridGap: 0,
        columnCount: 1,
        entries: [],
      };
    }

    const windowMargin = props.getWindowMargin(windowSize.height);
    const gridGap = props.getGridGap(elementWidth, windowSize.height);
    const columnCount = props.getColumnCount(elementWidth);
    const columnWidth = getColumnWidth(columnCount, gridGap, elementWidth);

    const entries = items.map((item) => {
      if (item.injected?.dateStoreGroupInfo) {
        return { ...item, columnSpan: columnCount };
      }

      if (!item.width) return item;

      const imageWidth = columnWidth * item.columnSpan + gridGap * (item.columnSpan - 1);
      return {
        ...item,
        height: props.getItemRatioHeight(item.height, item.width, imageWidth),
        width: imageWidth,
      };
    });

    return {
        windowMargin,
        gridGap,
        columnCount,
        entries,
    };
};

const computeLayoutData = (configData: ConfigData<any>): LayoutData<any> => {
  console.log('computeLayoutData');
    if (configData === null) {
        return { cells: [], totalHeight: 0 };
    }

    let currentRowNumber = 1;
    let prevRowsTotalHeight = 0;
    let currentRowMaxHeight = 0;

    let columnShift = 0;

    const cells: Cell<any>[] = configData.entries.map((entry, index) => {
        const { columnCount, gridGap } = configData;

        let columnSpanRecompute = entry.columnSpan;
        let heightRecompute = entry.height;

        // if the column span is 0 or negative we assume it is full width
        if (columnSpanRecompute < 1) {
            columnSpanRecompute = columnCount;
        }

        const distanceToRowStart = (index + columnShift) % columnCount;
        if (entry.newRow && distanceToRowStart !== 0) {
            columnShift += columnCount - distanceToRowStart;
        }

        const shiftedIndex = index + columnShift;
        const columnNumber = (shiftedIndex % columnCount) + 1;
        const rowNumber = Math.floor(shiftedIndex / columnCount) + 1;

        // here we check that an image is not going out of the grid, if yes we resize it
        if (columnNumber + columnSpanRecompute > columnCount + 1) {
            const overlapNumber = columnNumber + columnSpanRecompute - columnCount - 1;
            const overlapRatio = overlapNumber / columnSpanRecompute;
            heightRecompute = heightRecompute * (1 - overlapRatio);

            columnSpanRecompute -= overlapNumber;
        }

        // we need to count the shift created by multiple column objects
        if (columnSpanRecompute > 1) {
            columnShift += columnSpanRecompute - 1;
        }

        if (rowNumber !== currentRowNumber) {
            currentRowNumber = rowNumber;
            prevRowsTotalHeight += currentRowMaxHeight + gridGap;
            currentRowMaxHeight = 0;
        }

        const offset = prevRowsTotalHeight;
        const height = Math.round(heightRecompute);

        currentRowMaxHeight = Math.max(currentRowMaxHeight, height);

        return { ...entry, columnNumber, rowNumber, offset, height, columnSpan: columnSpanRecompute };
    });

    const totalHeight = prevRowsTotalHeight + currentRowMaxHeight;

    return { cells, totalHeight };
};

const computeRenderData = (
    configData: ConfigData<any>,
    elementWindowOffset: number,
    windowSize: { height: number, width: number },
    layoutData: LayoutData<any>
): RenderData<any> => {
  console.log('computeRenderData');
    if (layoutData === null || configData === null) {
        return { cellsToRender: [], firstRenderedRowNumber: 0, firstRenderedRowOffset: 0 };
    }
    const cellsToRender: Cell<any>[] = [];
    let firstRenderedRowNumber: null | number = null;
    let firstRenderedRowOffset: null | number = null;

    if (elementWindowOffset !== null) {
        for (const cell of layoutData.cells) {
            const cellTop = elementWindowOffset + cell.offset;
            const cellBottom = cellTop + cell.height;

            const windowBottom = windowSize.height;

            const renderTop = 0 - configData.windowMargin;
            const renderBottom = windowBottom + configData.windowMargin;

            if (cellTop > renderBottom) {
                continue;
            }
            if (cellBottom < renderTop) {
                continue;
            }

            if (firstRenderedRowNumber === null) {
                firstRenderedRowNumber = cell.rowNumber;
            }

            if (cell.rowNumber === firstRenderedRowNumber) {
                firstRenderedRowOffset = firstRenderedRowOffset
                    ? Math.min(firstRenderedRowOffset, cell.offset)
                    : cell.offset;
            }

            cellsToRender.push(cell);
        }
    }

    return { cellsToRender, firstRenderedRowNumber, firstRenderedRowOffset };
};

/** Grid utils */

function getColumnWidth(columnCount: number | null, gridGap: number | null, elementWidth: number | null) {
    if (columnCount === null || gridGap === null || elementWidth === null) {
        return 0;
    }

    const totalGapSpace = (columnCount - 1) * gridGap;
    const columnWidth = Math.round((elementWidth - totalGapSpace) / columnCount);

    return columnWidth;
}

function getGridRowStart(cell: Cell<any>, renderData: RenderData<any> | null) {
    if (renderData === null) {
        return undefined;
    }

    const offset = renderData.firstRenderedRowNumber !== null ? renderData.firstRenderedRowNumber - 1 : 0;
    const gridRowStart = cell.rowNumber - offset + rowOffsets.value.length;

    return `${gridRowStart}`;
}

/** For Parent Component */

function resetGrid(): void {
    bottomReached.value = false;
    loadMoreData();
}

/** Utils */

function isSameElementSize(a: ElementSize, b: ElementSize) {
    return a.width === b.width && a.height === b.height;
}

function getWindowSize(): ElementSize {
    return {
        width: window.innerWidth,
        height: window.innerHeight,
    };
}

function getElementSize(element: Element): ElementSize {
    const rect = element.getBoundingClientRect();
    return {
        width: rect.width,
        height: rect.height,
    };
}

function isSameElementScroll(a: ElementScroll, b: ElementScroll) {
    return a.x === b.x && a.y === b.y;
}

function getWindowScroll(): ElementScroll {
    return {
        x: window.scrollX,
        y: window.scrollY,
    };
}

function getElementOffset(element: Element) {
    return window.scrollY + element.getBoundingClientRect().top;
}

defineExpose({
    resetGrid
});
</script>


<template>
    <div
        ref="virtualGridRef"
        :style="{
            boxSizing: 'border-box',
            height: `${layoutData.totalHeight}px`,
            paddingTop: `${
                renderData !== null && renderData.firstRenderedRowOffset !== null
                    ? renderData.firstRenderedRowOffset + 'px'
                    : '0px'
            }`,
        }"
    >
        <div
            class="grid"
            :style="{
                'display': '-ms-grid',
                'display': 'grid',
                'align-items': 'center',
                'grid-template-columns': `repeat(${configData.columnCount}, 1fr)`,
                'gap': `${configData.gridGap}px`,
            }"
        >
            <div
                v-for="item in renderData.cellsToRender"
                :key="item.id"
                :class="`${item.injected.dateStoreGroupInfo ? 'banner grid-item-wrapper' : 'grid-item-wrapper'}`"
                :style="{
                    'height': `${item.height}px`,
                    'grid-column-start': item.columnNumber,
                    'grid-column-end': item.columnNumber + item.columnSpan,
                    'grid-row-start': getGridRowStart(item, renderData),
                }"
            >
                <component :is="item.injected.dateStoreGroupInfo ? props.headerComponent : props.itemComponent" :item="item" v-on="$listeners" />
            </div>
        </div>
    </div>
</template>

<style scoped>
.grid-item-wrapper {
  height: 150px !important;
}
.banner {
  display: block;
  height: 50px !important;
}
</style>

computeConfigData is called when i scroll when everything is done loading... after analyzing all my code and how the different parts of the VirtualGrid component interact together, and your extensive knowledge on Vue.js and Javascript, how to fix without changing anything crucial ? The goal here is improve performance drastically, because the computed functions are be re-evaluated to no avail.