import React, { forwardRef, HTMLAttributes, UIEvent, useEffect, useRef, useState } from 'react'
import PropTypes from 'prop-types'
import classNames from 'classnames'

import { useForkedRef } from '../../hooks'

export interface CVirtualScrollerProps extends Omit<HTMLAttributes<HTMLDivElement>, 'onScroll'> {
  /**
   * A string of all className you want applied to the base component.
   */
  className?: string
  /**
   * Event fires when the component has been scrolled.
   */
  onScroll?: (currentItemIndex: number) => void
  /**
   * Amount of visible items
   */
  visibleItems: number
}

export const CVirtualScroller = forwardRef<HTMLDivElement, CVirtualScrollerProps>(
  ({ children, className, visibleItems, onScroll, ...rest }, ref) => {
    const virtualScrollRef = useRef<HTMLDivElement>(null)
    const virtualScrollContentRef = useRef<HTMLDivElement>(null)
    const forkedRef = useForkedRef(ref, virtualScrollRef)

    const [buffer, setBuffer] = useState(Math.floor(visibleItems / 2))
    const [currentItemIndex, setCurrentItemIndex] = useState(1)
    const [itemHeight, setItemHeight] = useState(0)
    const [itemsNumber, setItemsNumber] = useState(React.Children.count(children))
    const [viewportPadding, setViewportPadding] = useState(0)
    const [viewportHeight, setViewportHeight] = useState(
      visibleItems * itemHeight + 2 * viewportPadding
    )
    const [maxHeight, setMaxHeight] = useState(itemsNumber * itemHeight + 2 * viewportPadding)

    useEffect(() => {
      if (virtualScrollRef.current) {
        void virtualScrollRef.current.scrollTop // force reflow
        setViewportPadding(Number.parseFloat(getComputedStyle(virtualScrollRef.current).paddingTop))
      }
    })

    useEffect(() => {
      setItemsNumber(React.Children.count(children))
    }, [children])

    useEffect(() => {
      setViewportHeight(Math.min(visibleItems, itemsNumber) * itemHeight + 2 * viewportPadding)
    }, [itemHeight, itemsNumber, viewportPadding, visibleItems])

    useEffect(() => {
      setMaxHeight(itemsNumber * itemHeight)
      void virtualScrollRef.current?.scrollTop // force reflow
    }, [itemHeight, itemsNumber])

    useEffect(() => {
      setBuffer(Math.floor(visibleItems / 2))
    }, [visibleItems])

    const handleScroll = (scrollTop: number) => {
      const _currentItemIndex = itemHeight ? Math.max(Math.ceil(scrollTop / itemHeight), 1) : 0
      setCurrentItemIndex(_currentItemIndex)
      onScroll?.(_currentItemIndex)
    }

    return (
      <div
        className={classNames('virtual-scroller', className)}
        onScroll={(event: UIEvent<HTMLDivElement>) => handleScroll(event.currentTarget.scrollTop)}
        style={{
          height: viewportHeight,
          overflowY: 'auto',
        }}
        ref={forkedRef}
        {...rest}
      >
        <div
          className="virtual-scroller-content"
          style={{
            height: maxHeight,
          }}
          ref={virtualScrollContentRef}
        >
          {React.Children.map(children, (child, index) => {
            if (
              React.isValidElement(child) &&
              index + 1 > Math.max(currentItemIndex - buffer, 0) &&
              index + 1 <= currentItemIndex + visibleItems + buffer
            ) {
              const childElement = child as React.ReactElement<Record<string, unknown>>
              return React.cloneElement(childElement, {
                className: classNames(childElement.props.className as string, {
                  'virtual-scroller-item-preload':
                    index + 1 > currentItemIndex + visibleItems || index + 1 < currentItemIndex,
                }),
                key: index,
                style: {
                  ...(currentItemIndex > buffer && {
                    transform: `translateY(${(currentItemIndex - buffer) * itemHeight}px)`,
                  }),
                },
                ref: (node: HTMLElement) =>
                  node &&
                  node.offsetHeight &&
                  setItemHeight(
                    node.offsetHeight +
                      Number.parseFloat(getComputedStyle(node).marginTop) +
                      Number.parseFloat(getComputedStyle(node).marginBottom)
                  ),
              })
            }
            return
          })}
        </div>
      </div>
    )
  }
)

CVirtualScroller.propTypes = {
  onScroll: PropTypes.func,
  visibleItems: PropTypes.number.isRequired,
}

CVirtualScroller.displayName = 'CVirtualScroller'
