<template>
  <div class="jogwheel-viewport" ref="jogwheelViewport" @scroll="handleJogwheelScroll">
    <div
      class="jogwheel-content"
      ref="jogwheelContent"
      :style="{ height: jogwheelHeight + 'px' }"
    ></div>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted, onBeforeUnmount } from 'vue'

const props = defineProps({
  totalVirtualWeeks: { type: Number, required: true },
  rowHeight: { type: Number, required: true },
  viewportHeight: { type: Number, required: true },
  scrollTop: { type: Number, required: true },
})

const emit = defineEmits(['scroll-to'])

const jogwheelViewport = ref(null)
const jogwheelContent = ref(null)
const syncLock = ref(null)
// Drag state (no momentum, 1:1 mapping)
const isDragging = ref(false)
let mainStartScroll = 0
let dragScale = 1 // mainScrollPixels per mouse pixel
let accumDelta = 0
let pointerLocked = false

// Jogwheel content height is 1/10th of main calendar
const jogwheelHeight = computed(() => {
  return (props.totalVirtualWeeks * props.rowHeight) / 10
})

const handleJogwheelScroll = () => {
  if (syncLock.value === 'jogwheel') return
  syncFromJogwheel()
}

function onDragMouseDown(e) {
  if (e.button !== 0) return
  isDragging.value = true
  mainStartScroll = props.scrollTop
  accumDelta = 0
  // Precompute scale between jogwheel scrollable range and main scrollable range
  const mainScrollable = Math.max(
    0,
    props.totalVirtualWeeks * props.rowHeight - props.viewportHeight,
  )
  let jogScrollable = 0
  if (jogwheelViewport.value && jogwheelContent.value) {
    jogScrollable = Math.max(
      0,
      jogwheelContent.value.scrollHeight - jogwheelViewport.value.clientHeight,
    )
  }
  dragScale = jogScrollable > 0 ? mainScrollable / jogScrollable : 1
  if (!isFinite(dragScale) || dragScale <= 0) dragScale = 1
  // Attempt pointer lock for relative movement
  if (jogwheelViewport.value && jogwheelViewport.value.requestPointerLock) {
    jogwheelViewport.value.requestPointerLock()
  }
  window.addEventListener('mousemove', onDragMouseMove, { passive: false })
  window.addEventListener('mouseup', onDragMouseUp, { passive: false })
  e.preventDefault()
}

function onDragMouseMove(e) {
  if (!isDragging.value) return
  const dy = pointerLocked ? e.movementY : e.clientY // movementY only valid in pointer lock
  accumDelta += dy
  let desired = mainStartScroll - accumDelta * dragScale
  if (desired < 0) desired = 0
  const maxScroll = Math.max(0, props.totalVirtualWeeks * props.rowHeight - props.viewportHeight)
  if (desired > maxScroll) desired = maxScroll
  emit('scroll-to', desired)
  e.preventDefault()
}

function onDragMouseUp(e) {
  if (!isDragging.value) return
  isDragging.value = false
  window.removeEventListener('mousemove', onDragMouseMove)
  window.removeEventListener('mouseup', onDragMouseUp)
  if (pointerLocked && document.exitPointerLock) document.exitPointerLock()
  e.preventDefault()
}

function handlePointerLockChange() {
  pointerLocked = document.pointerLockElement === jogwheelViewport.value
  if (!pointerLocked && isDragging.value) {
    // Pointer lock lost (Esc) -> end drag gracefully
    onDragMouseUp(new MouseEvent('mouseup'))
  }
}

onMounted(() => {
  if (jogwheelViewport.value) {
    jogwheelViewport.value.addEventListener('mousedown', onDragMouseDown)
  }
  document.addEventListener('pointerlockchange', handlePointerLockChange)
})

onBeforeUnmount(() => {
  if (jogwheelViewport.value) {
    jogwheelViewport.value.removeEventListener('mousedown', onDragMouseDown)
  }
  window.removeEventListener('mousemove', onDragMouseMove)
  window.removeEventListener('mouseup', onDragMouseUp)
  document.removeEventListener('pointerlockchange', handlePointerLockChange)
})

const syncFromJogwheel = () => {
  if (!jogwheelViewport.value || !jogwheelContent.value) return

  syncLock.value = 'main'

  const jogScrollable = Math.max(
    0,
    jogwheelContent.value.scrollHeight - jogwheelViewport.value.clientHeight,
  )
  const mainScrollable = Math.max(
    0,
    props.totalVirtualWeeks * props.rowHeight - props.viewportHeight,
  )

  if (jogScrollable > 0) {
    const ratio = jogwheelViewport.value.scrollTop / jogScrollable

    // Emit scroll event to parent to update main viewport
    emit('scroll-to', ratio * mainScrollable)
  }

  setTimeout(() => {
    if (syncLock.value === 'main') syncLock.value = null
  }, 50)
}

const syncFromMain = (mainScrollTop) => {
  if (!jogwheelViewport.value || !jogwheelContent.value) return
  if (syncLock.value === 'main') return

  syncLock.value = 'jogwheel'

  const mainScrollable = Math.max(
    0,
    props.totalVirtualWeeks * props.rowHeight - props.viewportHeight,
  )
  const jogScrollable = Math.max(
    0,
    jogwheelContent.value.scrollHeight - jogwheelViewport.value.clientHeight,
  )

  if (mainScrollable > 0) {
    const ratio = mainScrollTop / mainScrollable
    jogwheelViewport.value.scrollTop = ratio * jogScrollable
  }

  setTimeout(() => {
    if (syncLock.value === 'jogwheel') syncLock.value = null
  }, 50)
}

// Watch for main calendar scroll changes
watch(
  () => props.scrollTop,
  (newScrollTop) => {
    syncFromMain(newScrollTop)
  },
)

defineExpose({
  syncFromMain,
})
</script>

<style scoped>
.jogwheel-viewport {
  position: absolute;
  top: 0;
  inset-inline-end: 0;
  bottom: 0;
  width: var(--month-w);
  overflow-y: auto;
  overflow-x: hidden;
  scrollbar-width: none;
  z-index: 20;
  cursor: ns-resize;
}

.jogwheel-viewport::-webkit-scrollbar {
  display: none;
}

.jogwheel-content {
  position: relative;
  width: 100%;
}
</style>
