<script lang="ts" setup>
import { propTypes } from '@/utils/propTypes'
import { isClient, useEventListener, useWindowSize } from '@vueuse/core'
import type { CSSProperties } from 'vue'

defineOptions({ name: 'Sticky' })

const props = defineProps({
  // 距离顶部或者底部的距离(单位px)
  offset: propTypes.number.def(0),
  // 设置元素的堆叠顺序
  zIndex: propTypes.number.def(999),
  // 设置指定的class
  className: propTypes.string.def(''),
  // 定位方式，默认为(top)，表示距离顶部位置，可以设置为top或者bottom
  position: {
    type: String,
    validator: function (value: string) {
      return ['top', 'bottom'].indexOf(value) !== -1
    },
    default: 'top'
  }
})
const width = ref('auto' as string)
const height = ref('auto' as string)
const isSticky = ref(false)
const refSticky = shallowRef<HTMLElement>()
const scrollContainer = shallowRef<HTMLElement | Window>()
const { height: windowHeight } = useWindowSize()
onMounted(() => {
  height.value = refSticky.value?.getBoundingClientRect().height + 'px'

  scrollContainer.value = getScrollContainer(refSticky.value!, true)
  useEventListener(scrollContainer, 'scroll', handleScroll)
  useEventListener('resize', handleResize)
  handleScroll()
})
onActivated(() => {
  handleScroll()
})

const camelize = (str: string): string => {
  return str.replace(/-(\w)/g, (_, c) => (c ? c.toUpperCase() : ''))
}

const getStyle = (element: HTMLElement, styleName: keyof CSSProperties): string => {
  if (!isClient || !element || !styleName) return ''

  let key = camelize(styleName)
  if (key === 'float') key = 'cssFloat'
  try {
    const style = element.style[styleName]
    if (style) return style
    const computed = document.defaultView?.getComputedStyle(element, '')
    return computed ? computed[styleName] : ''
  } catch {
    return element.style[styleName]
  }
}
const isScroll = (el: HTMLElement, isVertical?: boolean): boolean => {
  if (!isClient) return false
  const key = (
    {
      undefined: 'overflow',
      true: 'overflow-y',
      false: 'overflow-x'
    } as const
  )[String(isVertical)]!
  const overflow = getStyle(el, key)
  return ['scroll', 'auto', 'overlay'].some((s) => overflow.includes(s))
}

const getScrollContainer = (
  el: HTMLElement,
  isVertical: boolean
): Window | HTMLElement | undefined => {
  if (!isClient) return
  let parent = el
  while (parent) {
    if ([window, document, document.documentElement].includes(parent)) return window
    if (isScroll(parent, isVertical)) return parent
    parent = parent.parentNode as HTMLElement
  }
  return parent
}

const handleScroll = () => {
  width.value = refSticky.value!.getBoundingClientRect().width! + 'px'
  if (props.position === 'top') {
    const offsetTop = refSticky.value?.getBoundingClientRect().top
    if (offsetTop !== undefined && offsetTop < props.offset) {
      sticky()
      return
    }
    reset()
  } else {
    const offsetBottom = refSticky.value?.getBoundingClientRect().bottom

    if (offsetBottom !== undefined && offsetBottom > windowHeight.value - props.offset) {
      sticky()
      return
    }
    reset()
  }
}
const handleResize = () => {
  if (isSticky.value && refSticky.value) {
    width.value = refSticky.value.getBoundingClientRect().width + 'px'
  }
}
const sticky = () => {
  if (isSticky.value) {
    return
  }
  isSticky.value = true
}
const reset = () => {
  if (!isSticky.value) {
    return
  }
  width.value = 'auto'
  isSticky.value = false
}
</script>
<template>
  <div ref="refSticky" :style="{ height: height, zIndex: zIndex }">
    <div
      :class="className"
      :style="{
        top: position === 'top' ? offset + 'px' : '',
        bottom: position !== 'top' ? offset + 'px' : '',
        zIndex: zIndex,
        position: isSticky ? 'fixed' : 'static',
        width: width,
        height: height
      }"
    >
      <slot>
        <div>sticky</div>
      </slot>
    </div>
  </div>
</template>
