import {
  h,
  ref,
  computed,
  watch,
  onActivated,
  onDeactivated,
  onBeforeMount,
  onBeforeUnmount,
  nextTick,
  getCurrentInstance
} from 'vue'

import debounce from '../../utils/debounce/debounce.js'
import { noop } from '../../utils/event/event.js'
import { rtlHasScrollBug } from '../../utils/private.rtl/rtl.js'

const aggBucketSize = 1000

const scrollToEdges = [
  'start',
  'center',
  'end',
  'start-force',
  'center-force',
  'end-force'
]

const filterProto = Array.prototype.filter

const setOverflowAnchor =
  __QUASAR_SSR__ ||
  window.getComputedStyle(document.body).overflowAnchor === void 0
    ? noop
    : function setOverflowAnchor(contentEl, index) {
        if (contentEl === null) return

        if (contentEl._qOverflowAnimationFrame !== void 0) {
          cancelAnimationFrame(contentEl._qOverflowAnimationFrame)
        }

        contentEl._qOverflowAnimationFrame = requestAnimationFrame(() => {
          if (contentEl === null) return

          contentEl._qOverflowAnimationFrame = void 0
          const children = contentEl.children || []

          filterProto
            .call(children, el => el.dataset && el.dataset.qVsAnchor !== void 0)
            .forEach(el => {
              delete el.dataset.qVsAnchor
            })

          const el = children[index]

          if (el?.dataset) {
            el.dataset.qVsAnchor = ''
          }
        })
      }

function sumFn(acc, item) {
  return acc + item
}

function getScrollDetails(
  parent,
  child,
  beforeRef,
  afterRef,
  horizontal,
  rtl,
  stickyStart,
  stickyEnd
) {
  const parentCalc =
      parent === window
        ? document.scrollingElement || document.documentElement
        : parent,
    propElSize = horizontal === true ? 'offsetWidth' : 'offsetHeight',
    details = {
      scrollStart: 0,
      scrollViewSize: -stickyStart - stickyEnd,
      scrollMaxSize: 0,
      offsetStart: -stickyStart,
      offsetEnd: -stickyEnd
    }

  if (horizontal === true) {
    if (parent === window) {
      details.scrollStart =
        window.pageXOffset || window.scrollX || document.body.scrollLeft || 0
      details.scrollViewSize += document.documentElement.clientWidth
    } else {
      details.scrollStart = parentCalc.scrollLeft
      details.scrollViewSize += parentCalc.clientWidth
    }
    details.scrollMaxSize = parentCalc.scrollWidth

    if (rtl === true) {
      details.scrollStart =
        (rtlHasScrollBug === true
          ? details.scrollMaxSize - details.scrollViewSize
          : 0) - details.scrollStart
    }
  } else {
    if (parent === window) {
      details.scrollStart =
        window.pageYOffset || window.scrollY || document.body.scrollTop || 0
      details.scrollViewSize += document.documentElement.clientHeight
    } else {
      details.scrollStart = parentCalc.scrollTop
      details.scrollViewSize += parentCalc.clientHeight
    }
    details.scrollMaxSize = parentCalc.scrollHeight
  }

  if (beforeRef !== null) {
    for (
      let el = beforeRef.previousElementSibling;
      el !== null;
      el = el.previousElementSibling
    ) {
      if (el.classList.contains('q-virtual-scroll--skip') === false) {
        details.offsetStart += el[propElSize]
      }
    }
  }

  if (afterRef !== null) {
    for (
      let el = afterRef.nextElementSibling;
      el !== null;
      el = el.nextElementSibling
    ) {
      if (el.classList.contains('q-virtual-scroll--skip') === false) {
        details.offsetEnd += el[propElSize]
      }
    }
  }

  if (child !== parent) {
    const parentRect = parentCalc.getBoundingClientRect(),
      childRect = child.getBoundingClientRect()

    if (horizontal === true) {
      details.offsetStart += childRect.left - parentRect.left
      details.offsetEnd -= childRect.width
    } else {
      details.offsetStart += childRect.top - parentRect.top
      details.offsetEnd -= childRect.height
    }

    if (parent !== window) {
      details.offsetStart += details.scrollStart
    }
    details.offsetEnd += details.scrollMaxSize - details.offsetStart
  }

  return details
}

function setScroll(parent, scroll, horizontal, rtl) {
  if (scroll === 'end') {
    scroll = (parent === window ? document.body : parent)[
      horizontal === true ? 'scrollWidth' : 'scrollHeight'
    ]
  }

  if (parent === window) {
    if (horizontal === true) {
      if (rtl === true) {
        scroll =
          (rtlHasScrollBug === true
            ? document.body.scrollWidth - document.documentElement.clientWidth
            : 0) - scroll
      }
      window.scrollTo(
        scroll,
        window.pageYOffset || window.scrollY || document.body.scrollTop || 0
      )
    } else {
      window.scrollTo(
        window.pageXOffset || window.scrollX || document.body.scrollLeft || 0,
        scroll
      )
    }
  } else if (horizontal === true) {
    if (rtl === true) {
      scroll =
        (rtlHasScrollBug === true
          ? parent.scrollWidth - parent.offsetWidth
          : 0) - scroll
    }
    parent.scrollLeft = scroll
  } else {
    parent.scrollTop = scroll
  }
}

function sumSize(sizeAgg, size, from, to) {
  if (from >= to) {
    return 0
  }

  const lastTo = size.length,
    fromAgg = Math.floor(from / aggBucketSize),
    toAgg = Math.floor((to - 1) / aggBucketSize) + 1

  let total = sizeAgg.slice(fromAgg, toAgg).reduce(sumFn, 0)

  if (from % aggBucketSize !== 0) {
    total -= size.slice(fromAgg * aggBucketSize, from).reduce(sumFn, 0)
  }
  if (to % aggBucketSize !== 0 && to !== lastTo) {
    total -= size.slice(to, toAgg * aggBucketSize).reduce(sumFn, 0)
  }

  return total
}

const commonVirtScrollProps = {
  virtualScrollSliceSize: {
    type: [Number, String],
    default: 10
  },

  virtualScrollSliceRatioBefore: {
    type: [Number, String],
    default: 1
  },

  virtualScrollSliceRatioAfter: {
    type: [Number, String],
    default: 1
  },

  virtualScrollItemSize: {
    type: [Number, String],
    default: 24
  },

  virtualScrollStickySizeStart: {
    type: [Number, String],
    default: 0
  },

  virtualScrollStickySizeEnd: {
    type: [Number, String],
    default: 0
  },

  tableColspan: [Number, String]
}

export const commonVirtScrollPropsList = Object.keys(commonVirtScrollProps)

export const useVirtualScrollProps = {
  virtualScrollHorizontal: Boolean,
  onVirtualScroll: Function,
  ...commonVirtScrollProps
}

export function useVirtualScroll({
  virtualScrollLength,
  getVirtualScrollTarget,
  getVirtualScrollEl,
  virtualScrollItemSizeComputed // optional
}) {
  const vm = getCurrentInstance()

  const { props, emit, proxy } = vm
  const { $q } = proxy

  let prevScrollStart,
    prevToIndex,
    localScrollViewSize,
    virtualScrollSizesAgg = [],
    virtualScrollSizes

  const virtualScrollPaddingBefore = ref(0)
  const virtualScrollPaddingAfter = ref(0)
  const virtualScrollSliceSizeComputed = ref({})

  const beforeRef = ref(null)
  const afterRef = ref(null)
  const contentRef = ref(null)

  const virtualScrollSliceRange = ref({ from: 0, to: 0 })

  const colspanAttr = computed(() =>
    props.tableColspan !== void 0 ? props.tableColspan : 100
  )

  if (virtualScrollItemSizeComputed === void 0) {
    virtualScrollItemSizeComputed = computed(() => props.virtualScrollItemSize)
  }

  const needsReset = computed(
    () =>
      virtualScrollItemSizeComputed.value + ';' + props.virtualScrollHorizontal
  )

  const needsSliceRecalc = computed(
    () =>
      needsReset.value +
      ';' +
      props.virtualScrollSliceRatioBefore +
      ';' +
      props.virtualScrollSliceRatioAfter
  )

  watch(needsSliceRecalc, () => {
    setVirtualScrollSize()
  })
  watch(needsReset, reset)

  function reset() {
    localResetVirtualScroll(prevToIndex, true)
  }

  function refresh(toIndex) {
    localResetVirtualScroll(toIndex === void 0 ? prevToIndex : toIndex)
  }

  function scrollTo(toIndex, edge) {
    const scrollEl = getVirtualScrollTarget()

    if (scrollEl === void 0 || scrollEl === null || scrollEl.nodeType === 8) {
      return
    }

    const scrollDetails = getScrollDetails(
      scrollEl,
      getVirtualScrollEl(),
      beforeRef.value,
      afterRef.value,
      props.virtualScrollHorizontal,
      $q.lang.rtl,
      props.virtualScrollStickySizeStart,
      props.virtualScrollStickySizeEnd
    )

    if (localScrollViewSize !== scrollDetails.scrollViewSize) {
      setVirtualScrollSize(scrollDetails.scrollViewSize)
    }

    setVirtualScrollSliceRange(
      scrollEl,
      scrollDetails,
      Math.min(
        virtualScrollLength.value - 1,
        Math.max(0, parseInt(toIndex, 10) || 0)
      ),
      0,
      scrollToEdges.indexOf(edge) !== -1
        ? edge
        : prevToIndex !== -1 && toIndex > prevToIndex
          ? 'end'
          : 'start'
    )
  }

  function localOnVirtualScrollEvt() {
    const scrollEl = getVirtualScrollTarget()

    if (scrollEl === void 0 || scrollEl === null || scrollEl.nodeType === 8) {
      return
    }

    const scrollDetails = getScrollDetails(
        scrollEl,
        getVirtualScrollEl(),
        beforeRef.value,
        afterRef.value,
        props.virtualScrollHorizontal,
        $q.lang.rtl,
        props.virtualScrollStickySizeStart,
        props.virtualScrollStickySizeEnd
      ),
      listLastIndex = virtualScrollLength.value - 1,
      listEndOffset =
        scrollDetails.scrollMaxSize -
        scrollDetails.offsetStart -
        scrollDetails.offsetEnd -
        virtualScrollPaddingAfter.value

    if (prevScrollStart === scrollDetails.scrollStart) return

    if (scrollDetails.scrollMaxSize <= 0) {
      setVirtualScrollSliceRange(scrollEl, scrollDetails, 0, 0)
      return
    }

    if (localScrollViewSize !== scrollDetails.scrollViewSize) {
      setVirtualScrollSize(scrollDetails.scrollViewSize)
    }

    updateVirtualScrollSizes(virtualScrollSliceRange.value.from)

    const scrollMaxStart = Math.floor(
      scrollDetails.scrollMaxSize -
        Math.max(scrollDetails.scrollViewSize, scrollDetails.offsetEnd) -
        Math.min(
          virtualScrollSizes[listLastIndex],
          scrollDetails.scrollViewSize / 2
        )
    )

    if (
      scrollMaxStart > 0 &&
      Math.ceil(scrollDetails.scrollStart) >= scrollMaxStart
    ) {
      setVirtualScrollSliceRange(
        scrollEl,
        scrollDetails,
        listLastIndex,
        scrollDetails.scrollMaxSize -
          scrollDetails.offsetEnd -
          virtualScrollSizesAgg.reduce(sumFn, 0)
      )

      return
    }

    let toIndex = 0,
      listOffset = scrollDetails.scrollStart - scrollDetails.offsetStart,
      offset = listOffset

    if (
      listOffset <= listEndOffset &&
      listOffset + scrollDetails.scrollViewSize >=
        virtualScrollPaddingBefore.value
    ) {
      listOffset -= virtualScrollPaddingBefore.value
      toIndex = virtualScrollSliceRange.value.from
      offset = listOffset
    } else {
      for (
        let j = 0;
        listOffset >= virtualScrollSizesAgg[j] && toIndex < listLastIndex;
        j++
      ) {
        listOffset -= virtualScrollSizesAgg[j]
        toIndex += aggBucketSize
      }
    }

    while (listOffset > 0 && toIndex < listLastIndex) {
      listOffset -= virtualScrollSizes[toIndex]
      if (listOffset > -scrollDetails.scrollViewSize) {
        toIndex++
        offset = listOffset
      } else {
        offset = virtualScrollSizes[toIndex] + listOffset
      }
    }

    setVirtualScrollSliceRange(scrollEl, scrollDetails, toIndex, offset)
  }

  function setVirtualScrollSliceRange(
    scrollEl,
    scrollDetails,
    toIndex,
    offset,
    align
  ) {
    const alignForce =
      typeof align === 'string' && align.indexOf('-force') !== -1
    const alignEnd = alignForce === true ? align.replace('-force', '') : align
    const alignRange = alignEnd !== void 0 ? alignEnd : 'start'

    let from = Math.max(
        0,
        toIndex - virtualScrollSliceSizeComputed.value[alignRange]
      ),
      to = from + virtualScrollSliceSizeComputed.value.total

    if (to > virtualScrollLength.value) {
      to = virtualScrollLength.value
      from = Math.max(0, to - virtualScrollSliceSizeComputed.value.total)
    }

    prevScrollStart = scrollDetails.scrollStart

    const rangeChanged =
      from !== virtualScrollSliceRange.value.from ||
      to !== virtualScrollSliceRange.value.to

    if (rangeChanged === false && alignEnd === void 0) {
      emitScroll(toIndex)
      return
    }

    const { activeElement } = document
    const contentEl = contentRef.value
    if (
      rangeChanged === true &&
      contentEl !== null &&
      contentEl !== activeElement &&
      contentEl.contains(activeElement) === true
    ) {
      contentEl.addEventListener('focusout', onBlurRefocusFn)

      setTimeout(() => {
        contentEl?.removeEventListener('focusout', onBlurRefocusFn)
      })
    }

    setOverflowAnchor(contentEl, toIndex - from)

    const sizeBefore =
      alignEnd !== void 0
        ? virtualScrollSizes.slice(from, toIndex).reduce(sumFn, 0)
        : 0

    if (rangeChanged === true) {
      // vue key matching algorithm works only if
      // the array of VNodes changes on only one of the ends
      // so we first change one end and then the other

      const tempTo =
        to >= virtualScrollSliceRange.value.from &&
        from <= virtualScrollSliceRange.value.to
          ? virtualScrollSliceRange.value.to
          : to

      virtualScrollSliceRange.value = { from, to: tempTo }
      virtualScrollPaddingBefore.value = sumSize(
        virtualScrollSizesAgg,
        virtualScrollSizes,
        0,
        from
      )
      virtualScrollPaddingAfter.value = sumSize(
        virtualScrollSizesAgg,
        virtualScrollSizes,
        to,
        virtualScrollLength.value
      )

      requestAnimationFrame(() => {
        if (
          virtualScrollSliceRange.value.to !== to &&
          prevScrollStart === scrollDetails.scrollStart
        ) {
          virtualScrollSliceRange.value = {
            from: virtualScrollSliceRange.value.from,
            to
          }
          virtualScrollPaddingAfter.value = sumSize(
            virtualScrollSizesAgg,
            virtualScrollSizes,
            to,
            virtualScrollLength.value
          )
        }
      })
    }

    requestAnimationFrame(() => {
      // if the scroll was changed give up
      // (another call to setVirtualScrollSliceRange before animation frame)
      if (prevScrollStart !== scrollDetails.scrollStart) return

      if (rangeChanged === true) {
        updateVirtualScrollSizes(from)
      }

      const sizeAfter = virtualScrollSizes
          .slice(from, toIndex)
          .reduce(sumFn, 0),
        posStart =
          sizeAfter +
          scrollDetails.offsetStart +
          virtualScrollPaddingBefore.value,
        posEnd = posStart + virtualScrollSizes[toIndex]

      let scrollPosition = posStart + offset

      if (alignEnd !== void 0) {
        const sizeDiff = sizeAfter - sizeBefore
        const scrollStart = scrollDetails.scrollStart + sizeDiff

        scrollPosition =
          alignForce !== true &&
          scrollStart < posStart &&
          posEnd < scrollStart + scrollDetails.scrollViewSize
            ? scrollStart
            : alignEnd === 'end'
              ? posEnd - scrollDetails.scrollViewSize
              : posStart -
                (alignEnd === 'start'
                  ? 0
                  : Math.round(
                      (scrollDetails.scrollViewSize -
                        virtualScrollSizes[toIndex]) /
                        2
                    ))
      }

      prevScrollStart = scrollPosition

      setScroll(
        scrollEl,
        scrollPosition,
        props.virtualScrollHorizontal,
        $q.lang.rtl
      )

      emitScroll(toIndex)
    })
  }

  function updateVirtualScrollSizes(from) {
    const contentEl = contentRef.value

    if (contentEl) {
      const children = filterProto.call(
          contentEl.children,
          el =>
            el.classList &&
            el.classList.contains('q-virtual-scroll--skip') === false
        ),
        childrenLength = children.length,
        sizeFn =
          props.virtualScrollHorizontal === true
            ? el => el.getBoundingClientRect().width
            : el => el.offsetHeight

      let index = from,
        size,
        diff

      for (let i = 0; i < childrenLength; ) {
        size = sizeFn(children[i])
        i++

        while (
          i < childrenLength &&
          children[i].classList.contains('q-virtual-scroll--with-prev') === true
        ) {
          size += sizeFn(children[i])
          i++
        }

        diff = size - virtualScrollSizes[index]

        if (diff !== 0) {
          virtualScrollSizes[index] += diff
          virtualScrollSizesAgg[Math.floor(index / aggBucketSize)] += diff
        }

        index++
      }
    }
  }

  function onBlurRefocusFn() {
    contentRef.value?.focus()
  }

  function localResetVirtualScroll(toIndex, fullReset) {
    const defaultSize = Number(virtualScrollItemSizeComputed.value)

    if (fullReset === true || Array.isArray(virtualScrollSizes) === false) {
      virtualScrollSizes = []
    }

    const oldVirtualScrollSizesLength = virtualScrollSizes.length

    virtualScrollSizes.length = virtualScrollLength.value

    for (
      let i = virtualScrollLength.value - 1;
      i >= oldVirtualScrollSizesLength;
      i--
    ) {
      virtualScrollSizes[i] = defaultSize
    }

    const jMax = Math.floor((virtualScrollLength.value - 1) / aggBucketSize)
    virtualScrollSizesAgg = []
    for (let j = 0; j <= jMax; j++) {
      let size = 0
      const iMax = Math.min((j + 1) * aggBucketSize, virtualScrollLength.value)
      for (let i = j * aggBucketSize; i < iMax; i++) {
        size += virtualScrollSizes[i]
      }
      virtualScrollSizesAgg.push(size)
    }

    prevToIndex = -1
    prevScrollStart = void 0

    virtualScrollPaddingBefore.value = sumSize(
      virtualScrollSizesAgg,
      virtualScrollSizes,
      0,
      virtualScrollSliceRange.value.from
    )
    virtualScrollPaddingAfter.value = sumSize(
      virtualScrollSizesAgg,
      virtualScrollSizes,
      virtualScrollSliceRange.value.to,
      virtualScrollLength.value
    )

    if (toIndex >= 0) {
      updateVirtualScrollSizes(virtualScrollSliceRange.value.from)
      nextTick(() => {
        scrollTo(toIndex)
      })
    } else {
      onVirtualScrollEvt()
    }
  }

  function setVirtualScrollSize(scrollViewSize) {
    if (scrollViewSize === void 0 && typeof window !== 'undefined') {
      const scrollEl = getVirtualScrollTarget()

      if (scrollEl !== void 0 && scrollEl !== null && scrollEl.nodeType !== 8) {
        scrollViewSize = getScrollDetails(
          scrollEl,
          getVirtualScrollEl(),
          beforeRef.value,
          afterRef.value,
          props.virtualScrollHorizontal,
          $q.lang.rtl,
          props.virtualScrollStickySizeStart,
          props.virtualScrollStickySizeEnd
        ).scrollViewSize
      }
    }

    localScrollViewSize = scrollViewSize

    const virtualScrollSliceRatioBefore =
      parseFloat(props.virtualScrollSliceRatioBefore) || 0
    const virtualScrollSliceRatioAfter =
      parseFloat(props.virtualScrollSliceRatioAfter) || 0
    const multiplier =
      1 + virtualScrollSliceRatioBefore + virtualScrollSliceRatioAfter
    const view =
      scrollViewSize === void 0 || scrollViewSize <= 0
        ? 1
        : Math.ceil(scrollViewSize / virtualScrollItemSizeComputed.value)

    const baseSize = Math.max(
      1,
      view,
      Math.ceil(
        (props.virtualScrollSliceSize > 0 ? props.virtualScrollSliceSize : 10) /
          multiplier
      )
    )

    virtualScrollSliceSizeComputed.value = {
      total: Math.ceil(baseSize * multiplier),
      start: Math.ceil(baseSize * virtualScrollSliceRatioBefore),
      center: Math.ceil(baseSize * (0.5 + virtualScrollSliceRatioBefore)),
      end: Math.ceil(baseSize * (1 + virtualScrollSliceRatioBefore)),
      view
    }
  }

  function padVirtualScroll(tag, content) {
    const paddingSize =
      props.virtualScrollHorizontal === true ? 'width' : 'height'
    const style = {
      ['--q-virtual-scroll-item-' + paddingSize]:
        virtualScrollItemSizeComputed.value + 'px'
    }

    return [
      tag === 'tbody'
        ? h(
            tag,
            {
              class: 'q-virtual-scroll__padding',
              key: 'before',
              ref: beforeRef
            },
            [
              h('tr', [
                h('td', {
                  style: {
                    [paddingSize]: `${virtualScrollPaddingBefore.value}px`,
                    ...style
                  },
                  colspan: colspanAttr.value
                })
              ])
            ]
          )
        : h(tag, {
            class: 'q-virtual-scroll__padding',
            key: 'before',
            ref: beforeRef,
            style: {
              [paddingSize]: `${virtualScrollPaddingBefore.value}px`,
              ...style
            }
          }),

      h(
        tag,
        {
          class: 'q-virtual-scroll__content',
          key: 'content',
          ref: contentRef,
          tabindex: -1
        },
        content.flat()
      ),

      tag === 'tbody'
        ? h(
            tag,
            {
              class: 'q-virtual-scroll__padding',
              key: 'after',
              ref: afterRef
            },
            [
              h('tr', [
                h('td', {
                  style: {
                    [paddingSize]: `${virtualScrollPaddingAfter.value}px`,
                    ...style
                  },
                  colspan: colspanAttr.value
                })
              ])
            ]
          )
        : h(tag, {
            class: 'q-virtual-scroll__padding',
            key: 'after',
            ref: afterRef,
            style: {
              [paddingSize]: `${virtualScrollPaddingAfter.value}px`,
              ...style
            }
          })
    ]
  }

  function emitScroll(index) {
    if (prevToIndex !== index) {
      if (props.onVirtualScroll !== void 0) {
        emit('virtualScroll', {
          index,
          from: virtualScrollSliceRange.value.from,
          to: virtualScrollSliceRange.value.to - 1,
          direction: index < prevToIndex ? 'decrease' : 'increase',
          ref: proxy
        })
      }

      prevToIndex = index
    }
  }

  setVirtualScrollSize()
  const onVirtualScrollEvt = debounce(
    localOnVirtualScrollEvt,
    $q.platform.is.ios === true ? 120 : 35
  )

  onBeforeMount(() => {
    setVirtualScrollSize()
  })

  let shouldActivate = false

  onDeactivated(() => {
    shouldActivate = true
  })

  onActivated(() => {
    if (shouldActivate !== true) return

    const scrollEl = getVirtualScrollTarget()

    if (
      prevScrollStart !== void 0 &&
      scrollEl !== void 0 &&
      scrollEl !== null &&
      scrollEl.nodeType !== 8
    ) {
      setScroll(
        scrollEl,
        prevScrollStart,
        props.virtualScrollHorizontal,
        $q.lang.rtl
      )
    } else {
      scrollTo(prevToIndex)
    }
  })

  if (!__QUASAR_SSR__) {
    onBeforeUnmount(() => {
      onVirtualScrollEvt.cancel()
    })
  }

  // expose public methods
  Object.assign(proxy, { scrollTo, reset, refresh })

  return {
    virtualScrollSliceRange,
    virtualScrollSliceSizeComputed,

    setVirtualScrollSize,
    onVirtualScrollEvt,
    localResetVirtualScroll,
    padVirtualScroll,

    scrollTo,
    reset,
    refresh
  }
}
