/**
 * Adapted from preact-virtual-list: https://github.com/developit/preact-virtual-list
 *
 * © 2016 Jason Miller
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 *
 * Adaptations:
 * - Added role=presentation to helper elements
 * - Tweaked styles for Uppy's Dashboard use case
 */

import type { ComponentChild, h } from 'preact'
import { useCallback, useEffect, useRef, useState } from 'preact/hooks'

const STYLE_INNER = {
  position: 'relative',
  // Disabled for our use case: the wrapper elements around FileList already deal with overflow,
  // and this additional property would hide things that we want to show.
  //
  // overflow: 'hidden',
  width: '100%',
  minHeight: '100%',
}

const STYLE_CONTENT = {
  position: 'absolute',
  top: 0,
  left: 0,
  // Because the `top` value gets set to some offset, this `height` being 100% would make the scrollbar
  // stretch far beyond the content. For our use case, the content div actually can get its height from
  // the elements inside it, so we don't need to specify a `height` property at all.
  //
  // height: '100%',
  width: '100%',
  overflow: 'visible',
}

export default function VirtualList<T>({
  data,
  rowHeight,
  renderRow,
  overscanCount = 10,
  padding = 4,
  ...props
}: {
  data: T[]
  rowHeight: number
  padding?: number // last visible + overscan row index + padding to allow keyboard focus to travel past the visible area
  renderRow: (item: T) => ComponentChild
  overscanCount?: number
} & h.JSX.HTMLAttributes<HTMLDivElement>) {
  const scrollerRef = useRef<HTMLDivElement>(null)

  const [offset, setOffset] = useState(0)
  const [height, setHeight] = useState(0)

  useEffect(() => {
    function resize() {
      if (
        scrollerRef.current != null &&
        height !== scrollerRef.current.offsetHeight
      ) {
        setHeight(scrollerRef.current.offsetHeight)
      }
    }

    resize()

    window.addEventListener('resize', resize)
    return () => {
      window.removeEventListener('resize', resize)
    }
  }, [height])

  const handleScroll = useCallback(() => {
    if (scrollerRef.current) setOffset(scrollerRef.current.scrollTop)
  }, [])

  // first visible row index
  let start = Math.floor(offset / rowHeight)

  // actual number of visible rows (without overscan)
  let visibleRowCount = Math.floor(height / rowHeight)

  // Overscan: render blocks of rows modulo an overscan row count
  // This dramatically reduces DOM writes during scrolling
  if (overscanCount) {
    start = Math.max(0, start - (start % overscanCount))
    visibleRowCount += overscanCount
  }

  const end = start + visibleRowCount + padding

  // data slice currently in viewport plus overscan items
  const selection = data.slice(start, end)

  const styleInner = { ...STYLE_INNER, height: data.length * rowHeight }
  const styleContent = { ...STYLE_CONTENT, top: start * rowHeight }

  // The `role="presentation"` attributes ensure that these wrapper elements are not treated as list
  // items by accessibility and outline tools.
  return (
    <div onScroll={handleScroll} ref={scrollerRef} {...props}>
      <div role="presentation" style={styleInner}>
        <div role="presentation" style={styleContent}>
          {selection.map(renderRow)}
        </div>
      </div>
    </div>
  )
}
