<template>
  <div class="timeline-container">
    <div
      class="timeline"
      ref="timelineRef"
      @scroll="onScroll"
      @wheel.prevent="onWheel"
      @touchstart.passive="onTouchStart"
      @touchmove.passive="onTouchMove"
      @touchend.passive="onTouchEnd"
    >
      <div class="timeline__track" :style="{ width: trackWidth + 'px' }">
        <!-- GlowDots — only visible ones rendered -->
        <GlowDot
          v-for="{ event, globalIndex } in visibleEvents"
          :key="event.id"
          :event="event"
          :x="getEventX(globalIndex)"
          :is-ghost="event.id === '__ghost__'"
          :selected="eventsStore.selectedEventId === event.id"
          @select="$emit('dotSelect', event.id)"
        />

        <!-- Month labels — only visible -->
        <div class="timeline__labels">
          <div
            v-for="{ label, globalIndex } in visibleLabels"
            :key="label.key"
            class="timeline__month"
            :style="{ left: getEventX(globalIndex) + 'px' }"
            :class="{ 'timeline__month--active': label.key === activeLabel }"
            @click="scrollToIndex(globalIndex)"
          >
            {{ label.month }}
          </div>
        </div>
      </div>
    </div>

    <!-- Sticky year labels with nav arrows (outside scroll container) -->
    <div class="timeline__sticky-years">
      <TransitionGroup name="year-fade">
        <div
          v-for="label in stickyYearLabels"
          :key="label.year"
          class="timeline__sticky-year-group"
          :style="{ left: label.left + 'px' }"
        >
          <button
            v-if="label.hasPrev"
            class="timeline__year-arrow timeline__year-arrow--prev"
            @click="scrollToYearCenter(label.year - 1)"
          >
            <svg width="14" height="14" viewBox="0 0 10 10"><path d="M7 1L3 5l4 4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
          </button>
          <span class="timeline__sticky-year" @click="scrollToYearCenter(label.year)">
            {{ label.year }}
          </span>
          <button
            v-if="label.hasNext"
            class="timeline__year-arrow timeline__year-arrow--next"
            @click="scrollToYearCenter(label.year + 1)"
          >
            <svg width="14" height="14" viewBox="0 0 10 10"><path d="M3 1l4 4-4 4" stroke="currentColor" stroke-width="1.5" fill="none" stroke-linecap="round" stroke-linejoin="round"/></svg>
          </button>
        </div>
      </TransitionGroup>
    </div>
  </div>
</template>

<script setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue'
import { useEventsStore } from 'stores/events'
import GlowDot from 'components/GlowDot.vue'

const emit = defineEmits(['dotSelect', 'viewUpdate'])
const eventsStore = useEventsStore()
const timelineRef = ref(null)
const scrollLeft = ref(0)
const viewportWidth = ref(400)
const containerHeight = ref(400)

// Zoom: 1.0 = default, range 0.4–3.0
const zoomLevel = ref(1)
const MIN_ZOOM = 0.4
const MAX_ZOOM = 3.0
const ZOOM_STEP = 0.08

// Spacing: ~4 events visible at a time, scaled by zoom
const BASE_SPACING = computed(() => viewportWidth.value / 2.5)
const EVENT_SPACING = computed(() => BASE_SPACING.value * zoomLevel.value)
const PADDING = computed(() => viewportWidth.value / 2)

// Display events: sorted events + ghost when creating
const showGhost = computed(() => eventsStore.panelOpen && !eventsStore.editingEventId)
const displayEvents = computed(() => {
  const sorted = [...eventsStore.sortedEvents]
  if (!showGhost.value) return sorted
  const ghost = eventsStore.ghostEvent
  const ghostDate = new Date(ghost.date)
  const insertIdx = sorted.findIndex((e) => new Date(e.date) > ghostDate)
  if (insertIdx === -1) {
    sorted.push(ghost)
  } else {
    sorted.splice(insertIdx, 0, ghost)
  }
  return sorted
})

// Track width
const trackWidth = computed(() => {
  const count = displayEvents.value.length
  if (count === 0) return viewportWidth.value
  return PADDING.value * 2 + (count - 1) * EVENT_SPACING.value
})

// X position for event at given index
function getEventX(index) {
  return PADDING.value + index * EVENT_SPACING.value
}

// Month names
const MONTHS = ['Jan', 'Feb', 'Mär', 'Apr', 'Mai', 'Jun', 'Jul', 'Aug', 'Sep', 'Okt', 'Nov', 'Dez']

// One label per event showing its month
const eventLabels = computed(() => {
  return displayEvents.value.map((event, index) => {
    const d = new Date(event.date)
    return {
      key: `${event.id}-${index}`,
      month: MONTHS[d.getMonth()],
      year: d.getFullYear(),
      fullMonth: `${d.getFullYear()}-${d.getMonth()}`
    }
  })
})

// Year ranges — for each year, the world-space X range of its events
const yearRanges = computed(() => {
  const ranges = []
  const events = displayEvents.value
  if (events.length === 0) return ranges

  let currentYear = new Date(events[0].date).getFullYear()
  let startIdx = 0

  for (let i = 1; i < events.length; i++) {
    const year = new Date(events[i].date).getFullYear()
    if (year !== currentYear) {
      ranges.push({ year: currentYear, startX: getEventX(startIdx), endX: getEventX(i - 1) })
      currentYear = year
      startIdx = i
    }
  }
  ranges.push({ year: currentYear, startX: getEventX(startIdx), endX: getEventX(events.length - 1) })
  return ranges
})

// All years that exist in the data (for prev/next navigation)
const allYears = computed(() => yearRanges.value.map(r => r.year))

// Sticky year labels — positioned relative to viewport, clamped to edges
const YEAR_MARGIN = 24
const stickyYearLabels = computed(() => {
  const sl = scrollLeft.value
  const vw = viewportWidth.value
  const viewLeft = sl
  const viewRight = sl + vw
  const years = allYears.value

  // Find years whose event range overlaps the viewport
  const visible = yearRanges.value.filter(r => r.endX >= viewLeft && r.startX <= viewRight)
  if (visible.length === 0) return []

  function makeLabel(year, left) {
    const idx = years.indexOf(year)
    return { year, left, hasPrev: idx > 0, hasNext: idx < years.length - 1 }
  }

  if (visible.length === 1) {
    const r = visible[0]
    const visStart = Math.max(r.startX, viewLeft)
    const visEnd = Math.min(r.endX, viewRight)
    const center = (visStart + visEnd) / 2 - sl
    const clamped = Math.max(YEAR_MARGIN, Math.min(vw - YEAR_MARGIN, center))
    return [makeLabel(r.year, clamped)]
  }

  // Multiple years: first pins left, last pins right, middles float naturally
  const result = []
  for (let i = 0; i < visible.length; i++) {
    const r = visible[i]
    const visStart = Math.max(r.startX, viewLeft)
    const visEnd = Math.min(r.endX, viewRight)
    const center = (visStart + visEnd) / 2 - sl

    let pos
    if (i === 0) {
      pos = Math.max(YEAR_MARGIN, Math.min(vw / 2 - 30, center))
    } else if (i === visible.length - 1) {
      pos = Math.min(vw - YEAR_MARGIN, Math.max(vw / 2 + 30, center))
    } else {
      pos = Math.max(YEAR_MARGIN + 60, Math.min(vw - YEAR_MARGIN - 60, center))
    }
    result.push(makeLabel(r.year, pos))
  }
  return result
})

// Virtualization: only render events near the viewport
const VIS_BUFFER = 2

const visibleRange = computed(() => {
  const total = displayEvents.value.length
  if (total === 0) return { start: 0, end: -1 }
  const spacing = EVENT_SPACING.value
  if (spacing <= 0) return { start: 0, end: total - 1 }
  const start = Math.max(0,
    Math.floor((scrollLeft.value - PADDING.value) / spacing) - VIS_BUFFER
  )
  const end = Math.min(total - 1,
    Math.ceil((scrollLeft.value + viewportWidth.value - PADDING.value) / spacing) + VIS_BUFFER
  )
  return { start, end }
})

const visibleEvents = computed(() => {
  const { start, end } = visibleRange.value
  if (end < start) return []
  return displayEvents.value.slice(start, end + 1).map((event, i) => ({
    event,
    globalIndex: start + i
  }))
})

const visibleLabels = computed(() => {
  const { start, end } = visibleRange.value
  if (end < start) return []
  return eventLabels.value.slice(start, end + 1).map((label, i) => ({
    label,
    globalIndex: start + i
  }))
})

// Active label — closest event to center of viewport (O(1) via index)
const activeLabel = computed(() => {
  const total = displayEvents.value.length
  if (total === 0) return null
  const centerX = scrollLeft.value + viewportWidth.value / 2
  const spacing = EVENT_SPACING.value
  if (spacing <= 0) return null
  const index = Math.round((centerX - PADDING.value) / spacing)
  const clamped = Math.max(0, Math.min(total - 1, index))
  return eventLabels.value[clamped]?.key ?? null
})

function onScroll() {
  if (timelineRef.value) {
    scrollLeft.value = timelineRef.value.scrollLeft
  }
}

function scrollToIndex(index) {
  if (!timelineRef.value) return
  const x = getEventX(index)
  timelineRef.value.scrollTo({
    left: x - viewportWidth.value / 2,
    behavior: 'smooth'
  })
}

function scrollToYearCenter(year) {
  if (!timelineRef.value) return
  // Find exact year, or nearest in the requested direction
  let range = yearRanges.value.find(r => r.year === year)
  if (!range) {
    // Find closest year
    const sorted = [...yearRanges.value].sort((a, b) => Math.abs(a.year - year) - Math.abs(b.year - year))
    range = sorted[0]
  }
  if (!range) return
  const centerX = (range.startX + range.endX) / 2
  timelineRef.value.scrollTo({
    left: centerX - viewportWidth.value / 2,
    behavior: 'smooth'
  })
}

function updateViewportWidth() {
  if (timelineRef.value) {
    viewportWidth.value = timelineRef.value.clientWidth || 400
    containerHeight.value = timelineRef.value.clientHeight || 400
  }
}

// Zoom while keeping the viewport center stable
function applyZoom(newZoom, centerClientX) {
  const el = timelineRef.value
  if (!el) return

  // Default center to viewport middle
  const rect = el.getBoundingClientRect()
  const cx = centerClientX !== undefined ? centerClientX - rect.left : viewportWidth.value / 2

  // World-space X under the center point before zoom
  const worldXBefore = el.scrollLeft + cx

  // Ratio of old spacing to new
  const oldZoom = zoomLevel.value
  const ratio = newZoom / oldZoom

  zoomLevel.value = newZoom

  // After Vue updates, restore scroll so the same world point stays under center
  nextTick(() => {
    el.scrollLeft = worldXBefore * ratio - cx
    scrollLeft.value = el.scrollLeft
  })
}

// Desktop: Ctrl+wheel or pinch on trackpad (both fire wheel with ctrlKey)
function onWheel(e) {
  if (e.ctrlKey || e.metaKey) {
    // Pinch / Ctrl+scroll → zoom
    const delta = -e.deltaY * ZOOM_STEP * 0.1
    const newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoomLevel.value + delta))
    if (newZoom !== zoomLevel.value) {
      applyZoom(newZoom, e.clientX)
    }
  } else {
    // Normal scroll — let the browser handle horizontal scroll
    const el = timelineRef.value
    if (el) {
      el.scrollLeft += e.deltaX || e.deltaY
      scrollLeft.value = el.scrollLeft
    }
  }
}

// Touch: pinch-to-zoom
let touchStartDist = 0
let touchStartZoom = 1

function getTouchDist(touches) {
  const dx = touches[0].clientX - touches[1].clientX
  const dy = touches[0].clientY - touches[1].clientY
  return Math.sqrt(dx * dx + dy * dy)
}

function onTouchStart(e) {
  if (e.touches.length === 2) {
    touchStartDist = getTouchDist(e.touches)
    touchStartZoom = zoomLevel.value
  }
}

function onTouchMove(e) {
  if (e.touches.length === 2) {
    const dist = getTouchDist(e.touches)
    const scale = dist / touchStartDist
    const newZoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, touchStartZoom * scale))
    const cx = (e.touches[0].clientX + e.touches[1].clientX) / 2
    applyZoom(newZoom, cx)
  }
}

function onTouchEnd() {
  touchStartDist = 0
}

// Scroll to center on the last event on mount
let resizeObserver = null
// Emit timeline state so the layout can position shader points
function emitViewState() {
  const { start, end } = visibleRange.value
  emit('viewUpdate', {
    scrollLeft: scrollLeft.value,
    viewportWidth: viewportWidth.value,
    containerHeight: containerHeight.value,
    visibleStart: start,
    visibleEnd: end,
    events: displayEvents.value.map((e, i) => ({
      emotion: e.emotion,
      x: getEventX(i),
      color: eventsStore.getGlowColor(e)
    }))
  })
}

watch(
  [scrollLeft, viewportWidth, containerHeight, displayEvents, zoomLevel],
  emitViewState,
  { deep: true }
)

onMounted(async () => {
  await nextTick()
  if (!timelineRef.value) return
  updateViewportWidth()

  const events = displayEvents.value
  if (events.length === 0) return
  const lastX = getEventX(events.length - 1)
  timelineRef.value.scrollLeft = lastX - viewportWidth.value / 2
  scrollLeft.value = timelineRef.value.scrollLeft

  // Update viewport width on resize
  resizeObserver = new ResizeObserver(updateViewportWidth)
  resizeObserver.observe(timelineRef.value)

  // Emit initial state
  emitViewState()
})

onUnmounted(() => {
  resizeObserver?.disconnect()
})

function zoomIn() {
  const newZoom = Math.min(MAX_ZOOM, zoomLevel.value + ZOOM_STEP * 2)
  if (newZoom !== zoomLevel.value) applyZoom(newZoom)
}

function zoomOut() {
  const newZoom = Math.max(MIN_ZOOM, zoomLevel.value - ZOOM_STEP * 2)
  if (newZoom !== zoomLevel.value) applyZoom(newZoom)
}

defineExpose({ timelineRef, zoomIn, zoomOut, zoomLevel, MIN_ZOOM, MAX_ZOOM })
</script>

<style scoped>
.timeline-container {
  position: absolute;
  top: 40px;
  left: 0;
  right: 0;
  bottom: 60px;
}

.timeline {
  position: absolute;
  inset: 0;
  overflow-x: auto;
  overflow-y: hidden;
  -webkit-overflow-scrolling: touch;
  scrollbar-width: none;
}

.timeline::-webkit-scrollbar {
  display: none;
}

.timeline__track {
  position: relative;
  height: 100%;
}

/* Month labels */
.timeline__labels {
  position: absolute;
  bottom: 44px;
  left: 0;
  right: 0;
  height: 24px;
}

.timeline__month {
  position: absolute;
  transform: translateX(-50%);
  font-size: 12px;
  font-weight: 400;
  opacity: 0.35;
  transition: opacity 0.3s ease, font-size 0.3s ease, font-weight 0.3s ease;
  white-space: nowrap;
  cursor: pointer;
}

.timeline__month--active {
  font-size: 15px;
  font-weight: 700;
  opacity: 1;
}

/* Sticky year labels — positioned outside the scroll container */
.timeline__sticky-years {
  position: absolute;
  bottom: 4px;
  left: 0;
  right: 0;
  height: 32px;
  pointer-events: none;
  z-index: 2;
}

.timeline__sticky-year-group {
  position: absolute;
  transform: translateX(-50%);
  display: flex;
  align-items: center;
  gap: 6px;
  pointer-events: auto;
  transition: left 0.15s ease-out;
}

.timeline__sticky-year {
  font-size: 16px;
  font-weight: 700;
  opacity: 0.6;
  white-space: nowrap;
  cursor: pointer;
}

.timeline__year-arrow {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 32px;
  height: 32px;
  border: none;
  border-radius: 50%;
  background: rgba(255, 255, 255, 0.1);
  color: inherit;
  opacity: 0.45;
  cursor: pointer;
  padding: 0;
  transition: opacity 0.2s ease, background 0.2s ease;
  -webkit-tap-highlight-color: transparent;
}

.timeline__year-arrow:active {
  opacity: 0.8;
  background: rgba(255, 255, 255, 0.25);
}

/* TransitionGroup animations */
.year-fade-enter-active,
.year-fade-leave-active {
  transition: opacity 0.3s ease;
}

.year-fade-enter-from,
.year-fade-leave-to {
  opacity: 0;
}
</style>
