<script lang="ts">
import type { Handler } from 'mitt'
import { useRafFn } from '@vueuse/core'
import mitt from 'mitt'
// eslint-disable-next-line import/no-duplicates
import { onBeforeUnmount, onMounted } from 'vue'

interface ScrollSyncEvent {
  scrollTop: number
  scrollHeight: number
  clientHeight: number
  scrollLeft: number
  scrollWidth: number
  clientWidth: number
  barHeight: number
  barWidth: number
  emitter: string
  group: string
}

type Events = {
  'scroll-sync': ScrollSyncEvent
}

const emitter = mitt<Events>()

function useEvent<Key extends keyof Events>(
  type: Key,
  handler: Handler<Events[Key]>,
): void {
  const { on, off } = emitter

  onMounted(() => {
    on(type, handler)
  })

  onBeforeUnmount(() => {
    off(type, handler)
  })
}
</script>

<script setup lang="ts">
// eslint-disable-next-line import/first, import/no-duplicates
import { useId, useTemplateRef } from 'vue'

const {
  proportional,
  vertical,
  horizontal,
  group,
} = defineProps<{
  proportional?: boolean
  vertical?: boolean
  horizontal?: boolean
  group: string
}>()

const uuid = useId()
const nodeRef = useTemplateRef('scroll-sync-container')

defineExpose({
  scrollTo: (options: ScrollToOptions) => {
    nodeRef.value?.scrollTo(options)
  },
})

function handleScroll(event: Event) {
  useRafFn(() => {
    const {
      scrollTop,
      scrollHeight,
      clientHeight,
      scrollLeft,
      scrollWidth,
      clientWidth,
      offsetHeight,
      offsetWidth,
    } = event.target as HTMLElement

    emitter.emit('scroll-sync', {
      scrollTop,
      scrollHeight,
      clientHeight,
      scrollLeft,
      scrollWidth,
      clientWidth,
      barHeight: offsetHeight - clientHeight,
      barWidth: offsetWidth - clientWidth,
      emitter: uuid,
      group,
    })
  }, { once: true })
}

useEvent('scroll-sync', (event: ScrollSyncEvent) => {
  const node = nodeRef.value

  if (event.group !== group || event.emitter === uuid || node === null) {
    return
  }

  const {
    scrollTop,
    scrollHeight,
    clientHeight,
    scrollLeft,
    scrollWidth,
    clientWidth,
    barHeight,
    barWidth,
  } = event

  // from https://github.com/okonet/react-scroll-sync
  const scrollTopOffset = scrollHeight - clientHeight
  const scrollLeftOffset = scrollWidth - clientWidth

  /* Calculate the actual pane height */
  const paneHeight = node.scrollHeight - clientHeight
  const paneWidth = node.scrollWidth - clientWidth

  /* Adjust the scrollTop position of it accordingly */
  node.removeEventListener('scroll', handleScroll)
  if (vertical && scrollTopOffset > barHeight) {
    node.scrollTop = proportional ? (paneHeight * scrollTop) / scrollTopOffset : scrollTop
  }
  if (horizontal && scrollLeftOffset > barWidth) {
    node.scrollLeft = proportional ? (paneWidth * scrollLeft) / scrollLeftOffset : scrollLeft
  }
  useRafFn(() => {
    node.addEventListener('scroll', handleScroll)
  }, { once: true })
})

onMounted(() => {
  const node = nodeRef.value
  node!.addEventListener('scroll', handleScroll)
})
</script>

<template>
  <div ref="scroll-sync-container" class="scroll-sync-container">
    <slot />
  </div>
</template>

<style scoped>
.scroll-sync-container {
  overflow: auto;
}
</style>
