/* Forked from react-virtualized and react-tiny-virtual-list and adapted to Hooks 💖 */
import * as React from 'react'
import {
  useResizeObserver,
  useForceUpdate,
  useInstanceRef
} from '@bestyled/contrib-common'
import SizeAndPositionManager, { ALIGNMENT } from './SizeAndPositionManager'

export interface RenderedRows {
  startIndex: number
  stopIndex: number
}

export interface FlatListProps<T> {
  id?: string
  className?: string
  data: T[]
  estimatedItemSize?: number
  height: number
  itemCount: number
  overscanCount?: number
  scrollToIndex?: number
  scrollToAlignment?: ALIGNMENT | string
  onScroll?(offset: number, event: UIEvent): void
  renderItem({ index: number, item: any }): React.ReactNode
}

export interface VisibleState {
  start?: number
  stop?: number
}

const STYLE_WRAPPER: React.CSSProperties = {
  overflow: 'auto',
  willChange: 'transform',
  WebkitOverflowScrolling: 'touch',
  boxSizing: 'content-box'
} as any

const STYLE_INNER: React.CSSProperties = {
  position: 'relative',
  display: 'flex',
  flexDirection: 'column',
  minHeight: '100%',
  marginRight: '10px'
}

const STYLE_ITEM: React.CSSProperties = {
  flex: '0 0 auto'
}

export const FlatListChild: React.FC<{
  measure: Function
  index: number
  style: React.CSSProperties
}> = React.memo(props => {
  const { measure, index, children, style } = props

  const [containerRef, height] = useResizeObserver()

  React.useEffect(() => {
    if (height !== null) {
      measure(index, height)
    }
  }, [height, index])

  return (
    <div
      itemType="FlatListChild"
      data-index={index}
      style={style}
      ref={containerRef as any}
    >
      {children}
    </div>
  )
})

export const FlatList: React.FC<FlatListProps<any>> = props => {
  const forceUpdate = useForceUpdate()

  /* prop variables */

  const {
    className,
    estimatedItemSize = 50,
    height,
    itemCount,
    overscanCount = 3,
    scrollToIndex,
    scrollToAlignment,
    onScroll,
    renderItem,
    data,
    id,
    ...rest
  } = props

  /* instance variables */

  const rootNode = React.useRef<HTMLDivElement>(null)

  const sizeAndPositionManager = useInstanceRef(
    () =>
      new SizeAndPositionManager({
        itemCount,
        estimatedItemSize
      })
  )

  const actualOffset = React.useRef<number>(0)

  const targetScrollToIndex = React.useRef<number | undefined>(scrollToIndex)

  React.useEffect(() => {
    if (scrollToIndex !== null) {
      targetScrollToIndex.current = scrollToIndex
      forceUpdate()
    }
  }, [scrollToIndex])

  React.useEffect(() => {
    sizeAndPositionManager.current.resize({
      itemCount,
      estimatedItemSize
    })

    forceUpdate()
  }, [itemCount, estimatedItemSize])

  const getStartStop = React.useCallback(() => {
    return sizeAndPositionManager.current.getVisibleRange({
      containerSize: height,
      offset: actualOffset.current,
      overscanCount
    })
  }, [height, overscanCount])

  const visibleRange = useInstanceRef(() => getStartStop())

  const getOffsetForIndex = React.useCallback(
    (index: number) => {
      if (index < 0 || index >= itemCount) {
        index = 0
      }

      return sizeAndPositionManager.current.getUpdatedOffsetForIndex({
        align: scrollToAlignment,
        containerSize: height,
        currentOffset: actualOffset.current || 0,
        targetIndex: index
      })
    },
    [scrollToAlignment, height, itemCount]
  )

  /* component mounted and unmounted */

  /* props change handlers */

  React.useEffect(() => {
    const { start, stop, paddingTop } = getStartStop()
    visibleRange.current.start = start
    visibleRange.current.stop = stop
    visibleRange.current.paddingTop = paddingTop
    forceUpdate()
  }, [
    getStartStop,
    itemCount,
    estimatedItemSize,
    height,
    overscanCount,
    scrollToIndex
  ])

  /* additional private methods */

  const scrollTo = (value: number) => {
    if (value == null) {
      return
    }

    rootNode.current!.scrollTo({
      top: value,
      behavior: rootNode.current!.scrollTop / value > 0.8 ? 'smooth' : 'auto'
    })
  }

  const handleScroll = React.useCallback(
    (event: UIEvent) => {
      const newOffset = getNodeOffset()

      if (
        newOffset < 0 ||
        actualOffset.current === newOffset ||
        event.target !== rootNode.current
      ) {
        return
      }

      actualOffset.current = newOffset

      const { start, stop, paddingTop } = getStartStop()

      if (
        start !== visibleRange.current.start ||
        stop !== visibleRange.current.stop
      ) {
        visibleRange.current.start = start
        visibleRange.current.stop = stop
        visibleRange.current.paddingTop = paddingTop
        forceUpdate()
      }

      if (typeof onScroll === 'function') {
        onScroll(newOffset, event)
      }
    },
    [getStartStop, onScroll]
  )

  React.useEffect(() => {
    rootNode.current!.addEventListener('scroll', handleScroll, {
      passive: true
    })

    if (scrollToIndex != null) {
      scrollTo(getOffsetForIndex(scrollToIndex))
    }

    return () => {
      rootNode.current!.removeEventListener('scroll', handleScroll)
    }
  }, [handleScroll])

  const getNodeOffset = () => {
    return rootNode.current!.scrollTop
  }

  const didChangeSize = React.useCallback(
    (index: number, height: number) => {
      if (!sizeAndPositionManager.current.setItem(index, height)) {
        return
      }

      if (targetScrollToIndex.current !== undefined && scrollToIndex !== null) {
        actualOffset.current =
          (targetScrollToIndex.current != null &&
            getOffsetForIndex(targetScrollToIndex.current)) ||
          0
      }

      const { start, stop, paddingTop } = getStartStop()

      if (
        start !== visibleRange.current.start ||
        stop !== visibleRange.current.stop
      ) {
        visibleRange.current.start = start
        visibleRange.current.stop = stop
        visibleRange.current.paddingTop = paddingTop
        forceUpdate()
      }

      if (index === targetScrollToIndex.current!) {
        setTimeout(() => {
          targetScrollToIndex.current = undefined
        }, 2000)
      }

      if (targetScrollToIndex.current !== undefined) {
        scrollTo(actualOffset.current)
      }
    },
    [scrollToAlignment, height, itemCount]
  )

  const items: React.ReactNode[] = []

  const wrapperStyle = {
    ...STYLE_WRAPPER,
    height
  }

  const innerStyle = {
    ...STYLE_INNER,
    height: sizeAndPositionManager.current.getTotalSize(),
    paddingTop: visibleRange.current.paddingTop
  }

  if (
    typeof visibleRange.current.start !== 'undefined' &&
    typeof visibleRange.current.stop !== 'undefined'
  ) {
    for (
      let index = visibleRange.current.start!;
      index <= visibleRange.current.stop!;
      index++
    ) {
      const item = data[index]
      items.push(
        <FlatListChild
          key={item ? item.key || -index : -index} // TO DO KeyExtractor if Needed
          measure={didChangeSize}
          index={index}
          style={STYLE_ITEM}
        >
          {renderItem({ item, index })}
        </FlatListChild>
      )
    }
  }

  return (
    <div ref={rootNode} {...rest} style={wrapperStyle}>
      <div
        id={id}
        itemType="FlatListInner"
        data-start={visibleRange.current.start}
        data-stop={visibleRange.current.stop}
        style={innerStyle}
      >
        {items}
      </div>
    </div>
  )
}

FlatList.defaultProps = {
  overscanCount: 3
}
