import {ComponentType, useCallback, useRef} from 'react'

import {
  VariableSizeList as _VariableSizeList,
  VariableSizeListProps,
} from 'react-window'

import {cn} from '../../lib/utils'
import '../../styles/utilities.css'

import {Pagination} from './pagination'

const VariableSizeList =
  _VariableSizeList as unknown as ComponentType<VariableSizeListProps>

interface Props<T = any>
  extends Omit<
    VariableSizeListProps<T>,
    'children' | 'itemCount' | 'width' | 'itemSize' | 'direction'
  > {
  items: T[]
  renderItem: (item: T, index: number) => React.ReactNode
  itemSizeForRow: (index: number) => number
  showScrollbar?: boolean
  header?: React.ReactNode
  headerHeight?: number
  fetchMore?: () => Promise<void>
}

export function List<T = any>({
  items,
  height,
  renderItem,
  className,
  showScrollbar = false,
  header,
  headerHeight,
  itemSizeForRow,
  fetchMore,
  overscanCount,
  ...listProps
}: Props<T>) {
  const inFlightFetchMoreRef = useRef<Promise<void> | null>(null)

  // This is workaround to prevent multiple calls to fetchMore
  // react-window re-renders the rows while scrolling,
  // and the TrackingPixel could be triggered multiple times

  const _fetchMore = useCallback(() => {
    // Dedupe concurrent calls by returning the same in-flight promise
    if (inFlightFetchMoreRef.current) return

    const current = Promise.resolve(fetchMore?.()).finally(() => {
      // Only clear if this is still the most recent promise
      if (inFlightFetchMoreRef.current === current) {
        inFlightFetchMoreRef.current = null
      }
    })

    inFlightFetchMoreRef.current = current
  }, [fetchMore])

  const rowRenderer = useCallback(
    ({index, style}: {index: number; style: React.CSSProperties}) => {
      // prepend the header to the first row if it exists
      if (header && index === 0) {
        return (
          <div style={style}>
            {header}
            <div style={{top: headerHeight}}>{renderItem(items[0], 0)}</div>
          </div>
        )
      }

      // append the pagination spinner to the last row if fetchMore exists
      if (fetchMore && index === items.length - 1) {
        return (
          <div style={style}>
            {renderItem(items[index], index)}
            <div style={{bottom: 0}}>
              <Pagination fetchMore={_fetchMore} />
            </div>
          </div>
        )
      }

      return <div style={style}>{renderItem(items[index], index)}</div>
    },
    [items, renderItem, header, headerHeight, _fetchMore, fetchMore]
  )

  const getItemSize = useCallback(
    (index: number) => {
      // include the header height in the first row height
      if (header && index === 0) {
        const _headerHeight = headerHeight || 0
        return _headerHeight + itemSizeForRow(index)
      }

      return itemSizeForRow(index)
    },
    [itemSizeForRow, header, headerHeight]
  )

  const classNames = cn(showScrollbar ? undefined : 'no-scrollbars', className)

  return (
    <VariableSizeList
      className={classNames}
      height={height}
      direction="vertical"
      width="100%"
      itemCount={items.length}
      overscanCount={overscanCount}
      itemSize={getItemSize}
      {...listProps}
    >
      {rowRenderer}
    </VariableSizeList>
  )
}
