import { memo, useCallback, useEffect, useMemo, useRef } from 'react'

import { Codicon } from '@/components/ui/codicon'

import type { TimeAxis } from './time-axis'

interface TimelineProps {
  axis: TimeAxis
  // Colour for memory stars — matches the map's memory glyph.
  memoryColor?: string
  onScrub: (reveal: number) => void
  onTogglePlay: () => void
  playing: boolean
  revealStore: RevealSignal
  // Reveal positions (0–1) where rings spawn — drawn as anchor ticks.
  ringStops?: number[]
}

interface RevealSignal {
  get: () => number
  subscribe: (listener: (value: number) => void) => () => void
}

interface Star {
  delay: number
  duration: number
  kind: 'memory' | 'skill'
  leftPct: number
  opacity: number
  size: number
  topPct: number
}

const ACTIVE_MARKER_CLASS = 'opacity-100'
const INACTIVE_MARKER_CLASS = 'opacity-30'
// Busiest bucket gets this many stars; quieter ones scale down proportionally.
const MAX_STARS_PER_BUCKET = 7

// Deterministic PRNG (mulberry32) so a bucket's stars stay put across renders.
function rng(seed: number): () => number {
  let a = seed >>> 0

  return () => {
    a += 0x6d2b79f5
    let t = a
    t = Math.imul(t ^ (t >>> 15), t | 1)
    t ^= t + Math.imul(t ^ (t >>> 7), t | 61)

    return ((t ^ (t >>> 14)) >>> 0) / 4294967296
  }
}

// Scatter each time bucket's activity into stars: count ∝ events, split between
// skill- and memory-coloured stars, jittered within the bucket's horizontal slot
// and across the track height. Vertical placement is biased toward the midline
// (a triangular distribution) so stars cluster near the centre more often than
// the edges. A starmap timeline for a starmap.
function buildStars(axis: TimeAxis): Star[] {
  const n = Math.max(1, axis.buckets.length)
  const stars: Star[] = []

  axis.buckets.forEach((b, i) => {
    if (b.total === 0) {
      return
    }

    const intensity = axis.maxTotal > 0 ? b.total / axis.maxTotal : 0
    // sqrt curve so a single co-timed burst (a packed core ring) doesn't crush
    // every quieter bucket down to the 1-star floor and read as blank.
    const count = Math.max(1, Math.round(Math.sqrt(intensity) * MAX_STARS_PER_BUCKET))
    const skillCount = Math.round((b.skill / b.total) * count)
    const r = rng(i * 9973 + 7)
    const slot = 1 / n
    const center = (i + 0.5) / n

    for (let s = 0; s < count; s++) {
      const jitter = (r() - 0.5) * slot * 0.9
      // Average of two uniforms → triangular peak at 0.5, pulling stars toward
      // the midline more often while still reaching the edges occasionally.
      const vertical = (r() + r()) / 2

      stars.push({
        delay: r() * 3,
        duration: 2.4 + r() * 2.6,
        kind: s < skillCount ? 'skill' : 'memory',
        leftPct: Math.max(0, Math.min(1, center + jitter)) * 100,
        // Brighter, slightly larger stars are rarer.
        opacity: 0.5 + r() * 0.5,
        // Floor at 2px so a lone star in a quiet bucket still reads on black.
        size: 2 + Math.round(r() * r() * 2.2),
        topPct: 12 + vertical * 76
      })
    }
  })

  return stars
}

// Playback scrubber as a constellation: dim stars are the unrevealed future; a
// scanner sweep ignites them (bright + twinkling) left→right as the reveal
// advances. The bright layer is clipped by the reveal CSS var, so the rAF sweep
// in StarMap drives it with zero per-frame JS.
export const Timeline = memo(function Timeline({
  axis,
  memoryColor = 'var(--theme-secondary)',
  onScrub,
  onTogglePlay,
  playing,
  revealStore,
  ringStops = []
}: TimelineProps) {
  const trackRef = useRef<HTMLDivElement | null>(null)
  const draggingRef = useRef(false)
  const markerRefs = useRef<HTMLDivElement[]>([])

  const stars = useMemo(() => buildStars(axis), [axis])

  const syncReveal = useCallback(
    (value: number) => {
      const reveal = Math.max(0, Math.min(1, value))
      const track = trackRef.current

      if (track) {
        track.style.setProperty('--starmap-reveal', String(reveal))
        track.setAttribute('aria-valuenow', String(Math.round(reveal * 100)))
      }

      ringStops.forEach((stop, i) => {
        const el = markerRefs.current[i]

        if (!el) {
          return
        }

        const active = stop <= reveal

        el.classList.toggle(ACTIVE_MARKER_CLASS, active)
        el.classList.toggle(INACTIVE_MARKER_CLASS, !active)
      })
    },
    [ringStops]
  )

  useEffect(() => revealStore.subscribe(syncReveal), [revealStore, syncReveal])

  useEffect(() => {
    markerRefs.current.length = ringStops.length
    syncReveal(revealStore.get())
  }, [revealStore, ringStops.length, syncReveal])

  const ratioAt = (clientX: number): number => {
    const rect = trackRef.current?.getBoundingClientRect()

    if (!rect || rect.width === 0) {
      return revealStore.get()
    }

    return Math.max(0, Math.min(1, (clientX - rect.left) / rect.width))
  }

  const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
    draggingRef.current = true
    e.currentTarget.setPointerCapture(e.pointerId)
    onScrub(ratioAt(e.clientX))
  }

  const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
    if (draggingRef.current) {
      onScrub(ratioAt(e.clientX))
    }
  }

  const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
    draggingRef.current = false

    if (e.currentTarget.hasPointerCapture(e.pointerId)) {
      e.currentTarget.releasePointerCapture(e.pointerId)
    }
  }

  const colorFor = (kind: Star['kind']) => (kind === 'skill' ? 'var(--theme-primary)' : memoryColor)

  return (
    <div className="pointer-events-auto flex w-[28rem] max-w-full items-center gap-3 [-webkit-app-region:no-drag]">
      <style>{'@keyframes starmap-twinkle{0%,100%{opacity:var(--o,1)}50%{opacity:calc(var(--o,1) * 0.35)}}'}</style>

      <button
        aria-label={playing ? 'Pause' : 'Play timeline'}
        className="flex size-5 shrink-0 items-center justify-center text-foreground/75 transition-colors hover:text-foreground"
        onClick={onTogglePlay}
        type="button"
      >
        <Codicon name={playing ? 'debug-pause' : 'triangle-right'} size={playing ? '0.8rem' : '0.95rem'} />
      </button>

      <div
        aria-label="Timeline scrubber"
        aria-valuemax={100}
        aria-valuemin={0}
        aria-valuenow={Math.round(revealStore.get() * 100)}
        className="relative h-7 min-w-0 flex-1 cursor-pointer select-none touch-none"
        onPointerDown={onPointerDown}
        onPointerMove={onPointerMove}
        onPointerUp={onPointerUp}
        ref={trackRef}
        role="slider"
        style={{ '--starmap-reveal': revealStore.get() } as React.CSSProperties}
        tabIndex={0}
      >
        {/* Dashed midline — a faint horizontal axis the stars ride over. */}
        <div
          aria-hidden
          className="pointer-events-none absolute inset-x-0 top-1/2 -translate-y-1/2 border-t border-dashed border-foreground/5"
        />

        {/* Dim constellation — the unrevealed future. */}
        <div aria-hidden className="absolute inset-0">
          {stars.map((star, i) => (
            <div
              className="absolute -translate-x-1/2 -translate-y-1/2 rounded-full"
              key={i}
              style={{
                backgroundColor: colorFor(star.kind),
                height: star.size,
                left: `${star.leftPct}%`,
                opacity: 0.22,
                top: `${star.topPct}%`,
                width: star.size
              }}
            />
          ))}
        </div>

        {/* Ignited constellation — bright + twinkling, clipped to the reveal. */}
        <div
          aria-hidden
          className="absolute inset-0"
          style={{ clipPath: 'inset(0 calc((1 - var(--starmap-reveal, 1)) * 100%) 0 0)' }}
        >
          {stars.map((star, i) => {
            const color = colorFor(star.kind)

            return (
              <div
                className="absolute -translate-x-1/2 -translate-y-1/2 rounded-full"
                key={i}
                style={
                  {
                    '--o': star.opacity,
                    animation: `starmap-twinkle ${star.duration}s ease-in-out ${star.delay}s infinite`,
                    backgroundColor: color,
                    height: star.size,
                    left: `${star.leftPct}%`,
                    opacity: star.opacity,
                    top: `${star.topPct}%`,
                    width: star.size
                  } as React.CSSProperties
                }
              />
            )
          })}
        </div>

        {/* Ring-spawn anchor ticks — small bright stars that light up on pass. */}
        <div aria-hidden className="absolute inset-0">
          {ringStops.map((stop, i) => (
            <div
              className={`pointer-events-none absolute top-1/2 size-1 -translate-x-1/2 -translate-y-1/2 rounded-full bg-[var(--theme-primary)] ${INACTIVE_MARKER_CLASS}`}
              key={i}
              ref={el => {
                if (el) {
                  markerRefs.current[i] = el
                }
              }}
              style={{ left: `${stop * 100}%` }}
            />
          ))}
        </div>

        {/* Playhead — a thin white sweep line. */}
        <div
          aria-hidden
          className="pointer-events-none absolute inset-y-0 w-0.5 -translate-x-1/2 bg-foreground"
          style={{ left: 'calc(var(--starmap-reveal, 1) * 100%)' }}
        />
      </div>
    </div>
  )
})
