// Components
import { makeDataTableProps } from './VDataTable'
import { VDataTableHeaders } from './VDataTableHeaders'
import { VDataTableRow } from './VDataTableRow'
import { VDataTableRows } from './VDataTableRows'
import { VTable } from '@/components/VTable'
import { VVirtualScrollItem } from '@/components/VVirtualScroll/VVirtualScrollItem'

// Composables
import { provideExpanded } from './composables/expand'
import { createGroupBy, makeDataTableGroupProps, provideGroupBy, useGroupedItems, useOpenAllGroups } from './composables/group'
import { createHeaders } from './composables/headers'
import { useDataTableItems } from './composables/items'
import { useOptions } from './composables/options'
import { provideSelection } from './composables/select'
import { createSort, provideSort, useSortedItems } from './composables/sort'
import { provideDefaults } from '@/composables/defaults'
import { makeFilterProps, useFilter } from '@/composables/filter'
import { MaybeTransition } from '@/composables/transition'
import { makeVirtualProps, useVirtual } from '@/composables/virtual'

// Utilities
import { cloneVNode, computed, Fragment, isVNode, nextTick, shallowRef, toRef, toRefs, watch } from 'vue'
import { convertToUnit, genericComponent, omit, pickWithRest, propsFactory, useRender } from '@/util'

// Types
import type { DeepReadonly, VNode, VNodeArrayChildren } from 'vue'
import type { VDataTableSlotProps } from './VDataTable'
import type { VDataTableHeadersSlots } from './VDataTableHeaders'
import type { VDataTableRowsSlots } from './VDataTableRows'
import type { CellProps, DataTableHeader, RowProps } from '@/components/VDataTable/types'
import type { GenericProps, SelectItemKey, TemplateRef } from '@/util'

type VDataTableVirtualSlotProps<T> = Omit<
  VDataTableSlotProps<T>,
  | 'setItemsPerPage'
  | 'page'
  | 'pageCount'
  | 'itemsPerPage'
  | 'prevPage'
  | 'nextPage'
  | 'setPage'
>

export type VDataTableVirtualSlots<T> = VDataTableRowsSlots<T> & VDataTableHeadersSlots & {
  caption: never
  colgroup: VDataTableVirtualSlotProps<T>
  top: VDataTableVirtualSlotProps<T>
  headers: VDataTableHeadersSlots['headers']
  tbody: VDataTableVirtualSlotProps<T>
  thead: VDataTableVirtualSlotProps<T>
  tfoot: VDataTableVirtualSlotProps<T>
  bottom: VDataTableVirtualSlotProps<T>
  'body.prepend': VDataTableVirtualSlotProps<T>
  'body.append': VDataTableVirtualSlotProps<T>
  item: {
    itemRef: TemplateRef
  }
}

export const makeVDataTableVirtualProps = propsFactory({
  ...omit(makeDataTableProps(), ['hideDefaultFooter']),
  ...makeDataTableGroupProps(),
  ...makeVirtualProps(),
  ...makeFilterProps(),
}, 'VDataTableVirtual')

type ItemType<T> = T extends readonly (infer U)[] ? U : never

function elementNodes (nodes?: VNodeArrayChildren): VNode[] {
  return (nodes ?? []).flatMap(node =>
    Array.isArray(node) ? elementNodes(node)
    : !isVNode(node) ? []
    : node.type === Fragment ? elementNodes(node.children as VNodeArrayChildren)
    : typeof node.type === 'symbol' ? []
    : [node]
  )
}

export const VDataTableVirtual = genericComponent<new <T extends readonly any[], V>(
  props: {
    items?: T
    itemValue?: SelectItemKey<ItemType<T>>
    rowProps?: RowProps<ItemType<T>>
    cellProps?: CellProps<ItemType<T>>
    itemSelectable?: SelectItemKey<ItemType<T>>
    headers?: DeepReadonly<DataTableHeader<ItemType<T>>[]>
    modelValue?: V
    'onUpdate:modelValue'?: (value: V) => void
  },
  slots: VDataTableVirtualSlots<ItemType<T>>,
) => GenericProps<typeof props, typeof slots>>()({
  name: 'VDataTableVirtual',

  props: makeVDataTableVirtualProps(),

  emits: {
    'update:modelValue': (value: any[]) => true,
    'update:sortBy': (value: any) => true,
    'update:options': (value: any) => true,
    'update:groupBy': (value: any) => true,
    'update:expanded': (value: any) => true,
    'update:opened': (value: string[]) => true,
  },

  setup (props, { attrs, slots }) {
    const { groupBy, opened, openAll, groupKey } = createGroupBy(props)
    const { initialSortOrder, sortBy, multiSort, mustSort } = createSort(props)
    const { disableSort } = toRefs(props)

    const {
      columns,
      headers,
      filterFunctions,
      sortFunctions,
      sortRawFunctions,
    } = createHeaders(props, {
      groupBy,
      showSelect: toRef(() => props.showSelect),
      showExpand: toRef(() => props.showExpand),
    })
    const { items } = useDataTableItems(props, columns)

    const search = toRef(() => props.search)
    const { filteredItems, getMatches } = useFilter(props, items, search, {
      transform: item => item.columns,
      customKeyFilter: filterFunctions,
    })

    const { toggleSort } = provideSort({ initialSortOrder, sortBy, multiSort, mustSort })
    const {
      sortByWithGroups,
      opened: openedGroups,
      extractRows,
      isGroupOpen,
      toggleGroup,
    } = provideGroupBy({ groupBy, sortBy, disableSort, opened })

    const { sortedItems } = useSortedItems(props, filteredItems, sortByWithGroups, {
      transform: item => ({ ...item.raw, ...item.columns }),
      sortFunctions,
      sortRawFunctions,
    })
    useOpenAllGroups(openedGroups, openAll, sortedItems, groupBy, groupKey)
    const { flatItems } = useGroupedItems(sortedItems, groupBy, openedGroups, () => !!slots['group-summary'], isGroupOpen, groupKey)

    const allItems = computed(() => extractRows(flatItems.value))

    const { isSelected, select, selectAll, toggleSelect, someSelected, allSelected } = provideSelection(props, {
      allItems,
      currentPage: allItems,
    })
    const { expanded, isExpanded, toggleExpand } = provideExpanded(props)

    const {
      containerRef,
      markerRef,
      paddingTop,
      paddingBottom,
      computedItems,
      handleItemResize,
      handleScroll,
      handleScrollend,
      calculateVisibleItems,
      scrollToIndex,
    } = useVirtual(props, flatItems)

    const displayItems = computed(() =>
      computedItems.value
        .map(item => ({
          ...item.raw,
          virtualIndex: item.index,
        }))
    )

    // one virtual size per index = item row + optional expanded rows
    const rowHeights = new Map<number, number>()
    const expandedHeights = new Map<number, number[]>()

    function updateSize (index: number) {
      const expanded = expandedHeights.get(index)?.reduce((sum, height) => sum + (height || 0), 0) ?? 0
      handleItemResize(index, (rowHeights.get(index) ?? 0) + expanded)
    }

    function setRowHeight (index: number, height: number) {
      rowHeights.set(index, height)
      updateSize(index)
    }

    function setExpandedHeight (index: number, row: number, height: number) {
      const heights = expandedHeights.get(index) ?? []
      heights[row] = height
      expandedHeights.set(index, heights)
      updateSize(index)
    }

    function trimExpandedHeights (index: number, count: number) {
      const heights = expandedHeights.get(index)
      if (!heights || heights.length <= count) return

      heights.length = count
      updateSize(index)
    }

    watch(expanded, () => {
      if (!slots['expanded-row']) return
      for (const index of [...expandedHeights.keys()]) {
        const item = flatItems.value[index]
        if (item?.type === 'item' && isExpanded(item)) continue
        expandedHeights.delete(index)
        updateSize(index)
      }
    })

    watch(flatItems, () => {
      rowHeights.clear()
      expandedHeights.clear()
    })

    useOptions({
      sortBy,
      page: shallowRef(1),
      itemsPerPage: shallowRef(-1),
      groupBy,
      search,
    })

    provideDefaults({
      VDataTableRows: {
        hideNoData: toRef(() => props.hideNoData),
        noDataText: toRef(() => props.noDataText),
        loading: toRef(() => props.loading),
        loadingText: toRef(() => props.loadingText),
      },
    })

    const slotProps = computed<VDataTableVirtualSlotProps<any>>(() => ({
      itemsLength: allItems.value.length,
      sortBy: sortBy.value,
      toggleSort,
      someSelected: someSelected.value,
      allSelected: allSelected.value,
      isSelected,
      select,
      selectAll,
      toggleSelect,
      isExpanded,
      toggleExpand,
      isGroupOpen,
      toggleGroup,
      items: allItems.value.map(item => item.raw),
      internalItems: allItems.value,
      groupedItems: flatItems.value,
      columns: columns.value,
      headers: headers.value,
    }))

    useRender(() => {
      const dataTableHeadersProps = VDataTableHeaders.filterProps(omit(props, ['multiSort']))
      const dataTableRowsProps = VDataTableRows.filterProps(props)
      const tableProps = VTable.filterProps(props)
      const [tableAttrs] = pickWithRest(attrs, [/^aria-label/])

      return (
        <VTable
          class={[
            'v-data-table',
            {
              'v-data-table--loading': props.loading,
            },
            props.class,
          ]}
          style={ props.style }
          { ...tableProps }
          fixedHeader={ props.fixedHeader || props.sticky }
        >
          {{
            top: () => slots.top?.(slotProps.value),
            wrapper: () => (
              <div
                ref={ containerRef }
                onScrollPassive={ handleScroll }
                onScrollend={ handleScrollend }
                class="v-table__wrapper"
                style={{
                  height: convertToUnit(props.height),
                }}
              >
                <table { ...tableAttrs }>
                  { slots.caption?.() }
                  { slots.colgroup?.(slotProps.value) }
                  { !props.hideDefaultHeader && (
                    <thead key="thead">
                      <VDataTableHeaders
                        { ...dataTableHeadersProps }
                        multiSort={ !!props.multiSort }
                        v-slots={ slots }
                      />
                    </thead>
                  )}
                  { slots.thead?.(slotProps.value) }
                  { !props.hideDefaultBody && (
                    <tbody key="tbody">
                      <tr ref={ markerRef } style={{ height: convertToUnit(paddingTop.value), border: 0 }}>
                        <td colspan={ columns.value.length } style={{ height: 0, border: 0 }}></td>
                      </tr>

                      { slots['body.prepend']?.(slotProps.value) }

                      <VDataTableRows
                        { ...attrs }
                        { ...dataTableRowsProps }
                        items={ displayItems.value }
                        getMatches={ getMatches }
                      >
                        {{
                          ...omit(slots, ['expanded', 'expanded-row']),
                          item: itemSlotProps => {
                            const { props: rowProps, ...itemSlot } = itemSlotProps
                            const index = itemSlotProps.internalItem.virtualIndex ?? itemSlotProps.internalItem.index
                            const itemExpanded = isExpanded(itemSlotProps.internalItem)
                            const expandedRows = props.showExpand && itemExpanded && slots['expanded-row']
                              ? elementNodes(slots['expanded-row'](itemSlot))
                              : []

                            // rows that stay never resize, so a shrunk slot reports nothing
                            if (slots['expanded-row'] && (expandedHeights.get(index)?.length ?? 0) > expandedRows.length) {
                              nextTick(() => trimExpandedHeights(index, expandedRows.length))
                            }

                            return (
                              <>
                                <VVirtualScrollItem
                                  key={ index }
                                  renderless
                                  onUpdate:height={ height => setRowHeight(index, height) }
                                >
                                  { ({ itemRef }) => (
                                    slots.item?.({ ...itemSlotProps, itemRef }) ?? (
                                      <VDataTableRow
                                        { ...rowProps }
                                        ref={ itemRef }
                                        index={ itemSlotProps.index }
                                        getMatches={ getMatches }
                                        v-slots={ slots }
                                      />
                                    )
                                  )}
                                </VVirtualScrollItem>

                                { props.showExpand && (
                                  slots['expanded-row']
                                    ? expandedRows.map((node, row) => (
                                      <VVirtualScrollItem
                                        key={ `${index}-expanded-${row}` }
                                        renderless
                                        onUpdate:height={ height => setExpandedHeight(index, row, height) }
                                      >
                                        { ({ itemRef }) => cloneVNode(node, { ref: itemRef }, true) }
                                      </VVirtualScrollItem>
                                    ))
                                    : slots.expanded && (
                                      <VVirtualScrollItem
                                        key={ `${index}-expanded` }
                                        renderless
                                        onUpdate:height={ height => setExpandedHeight(index, 0, height) }
                                      >
                                        { ({ itemRef }) => (
                                          <tr class="v-data-table__tr--expanded" ref={ itemRef }>
                                            <td colspan={ columns.value.length }>
                                              { props.expandTransition
                                                ? (
                                                  <MaybeTransition transition={ props.expandTransition }>
                                                    { itemExpanded
                                                      ? <div>{ slots.expanded!(itemSlot) }</div>
                                                      : null }
                                                  </MaybeTransition>
                                                )
                                                // bypass <Transition> to avoid flash when expand-strategy=single
                                                : itemExpanded && <div>{ slots.expanded!(itemSlot) }</div>
                                              }
                                            </td>
                                          </tr>
                                        )}
                                      </VVirtualScrollItem>
                                    )
                                )}
                              </>
                            )
                          },
                        }}
                      </VDataTableRows>

                      { slots['body.append']?.(slotProps.value) }

                      <tr style={{ height: convertToUnit(paddingBottom.value), border: 0 }}>
                        <td colspan={ columns.value.length } style={{ height: 0, border: 0 }}></td>
                      </tr>
                    </tbody>
                  )}
                  { slots.tbody?.(slotProps.value) }
                  { slots.tfoot?.(slotProps.value) }
                </table>
              </div>
            ),
            bottom: () => slots.bottom?.(slotProps.value),
          }}
        </VTable>
      )
    })

    return {
      calculateVisibleItems,
      scrollToIndex,
    }
  },
})

export type VDataTableVirtual = InstanceType<typeof VDataTableVirtual>
