<script lang="ts">
  interface Props {
    value: string;
    onchange?: (value: string) => void;
    countdownFont?: string;
  }

  let { value, onchange, countdownFont = "" }: Props = $props();

  // Local display values — driven by prop normally, overridden during drag/momentum
  let displayHours = $state(0);
  let displayMinutes = $state(0);
  let isAnimating = $state(false); // true during drag OR momentum

  // Fractional offset for smooth wheel rotation (0 to <1)
  let hoursFraction = $state(0);
  let minutesFraction = $state(0);

  // Sync display from prop when NOT dragging/animating
  $effect(() => {
    if (!isAnimating) {
      displayHours = parseInt(value.split(":")[0]) || 0;
      displayMinutes = parseInt(value.split(":")[1]) || 0;
      hoursFraction = 0;
      minutesFraction = 0;
    }
  });

  // ── Drag state ──

  let hoursDragging = $state(false);
  let hoursLastY = $state(0);
  let hoursAccumPx = $state(0);
  let hoursBaseValue = $state(0);
  let hoursVelocity = $state(0);
  let hoursLastMoveTime = $state(0);

  let minutesDragging = $state(false);
  let minutesLastY = $state(0);
  let minutesAccumPx = $state(0);
  let minutesBaseValue = $state(0);
  let minutesVelocity = $state(0);
  let minutesLastMoveTime = $state(0);

  // Momentum animation handle
  let momentumRaf: number | null = null;

  const SENSITIVITY = 20;   // pixels per value step
  const ITEM_ANGLE = 30;    // degrees between items on the cylinder
  const WHEEL_RADIUS = 26;  // cylinder radius in px
  const FRICTION = 0.93;    // momentum decay per frame
  const MIN_VELOCITY = 0.3; // px/frame threshold to stop momentum

  function wrapValue(v: number, max: number): number {
    return ((v % max) + max) % max;
  }

  function emitValue(h: number, m: number) {
    onchange?.(`${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`);
  }

  function stopMomentum() {
    if (momentumRaf !== null) {
      cancelAnimationFrame(momentumRaf);
      momentumRaf = null;
    }
  }

  // Build the visible items for the 3D wheel
  function getWheelItems(current: number, fraction: number, maxVal: number) {
    const items = [];
    for (let i = -2; i <= 2; i++) {
      const val = wrapValue(current + i, maxVal);
      const angle = (i - fraction) * ITEM_ANGLE;
      const absAngle = Math.abs(angle);
      // Center item bright, off-center very dim
      const isCenter = absAngle < ITEM_ANGLE * 0.5;
      const opacity = isCenter
        ? Math.max(0.7, 1 - absAngle / 60)
        : Math.max(0.05, 0.35 - absAngle / 120);
      items.push({ value: val, angle, opacity });
    }
    return items;
  }

  const hoursItems = $derived(getWheelItems(displayHours, hoursFraction, 24));
  const minutesItems = $derived(getWheelItems(displayMinutes, minutesFraction, 60));

  // ── Shared step logic (used by both drag and momentum) ──

  function applyAccum(
    field: "hours" | "minutes",
    accumPx: number,
    baseValue: number,
  ): { newVal: number; fraction: number } {
    const totalSteps = accumPx / SENSITIVITY;
    const wholeSteps = Math.round(totalSteps);
    const fraction = totalSteps - wholeSteps;
    const maxVal = field === "hours" ? 24 : 60;
    const newVal = wrapValue(baseValue + wholeSteps, maxVal);
    return { newVal, fraction };
  }

  // ── Hours pointer handlers ──

  function handleHoursPointerDown(e: PointerEvent) {
    e.preventDefault();
    e.stopPropagation();
    stopMomentum();
    hoursDragging = true;
    isAnimating = true;
    hoursLastY = e.clientY;
    hoursAccumPx = 0;
    hoursBaseValue = displayHours;
    hoursVelocity = 0;
    hoursLastMoveTime = performance.now();
    (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
  }

  function handleHoursPointerMove(e: PointerEvent) {
    if (!hoursDragging) return;
    e.preventDefault();
    const dy = hoursLastY - e.clientY;
    hoursAccumPx += dy;
    hoursLastY = e.clientY;

    // Track velocity (px/ms)
    const now = performance.now();
    const dt = now - hoursLastMoveTime;
    if (dt > 0) hoursVelocity = dy / dt;
    hoursLastMoveTime = now;

    const { newVal, fraction } = applyAccum("hours", hoursAccumPx, hoursBaseValue);
    hoursFraction = fraction;
    if (newVal !== displayHours) {
      displayHours = newVal;
      emitValue(newVal, displayMinutes);
    }
  }

  function handleHoursPointerUp(e: PointerEvent) {
    if (!hoursDragging) return;
    e.preventDefault();
    hoursDragging = false;

    // Launch momentum if velocity is significant
    const velocityPxPerFrame = hoursVelocity * 16; // convert px/ms to px/frame (~16ms)
    if (Math.abs(velocityPxPerFrame) > MIN_VELOCITY) {
      startMomentum("hours", velocityPxPerFrame);
    } else {
      hoursFraction = 0;
      isAnimating = false;
    }
  }

  // ── Minutes pointer handlers ──

  function handleMinutesPointerDown(e: PointerEvent) {
    e.preventDefault();
    e.stopPropagation();
    stopMomentum();
    minutesDragging = true;
    isAnimating = true;
    minutesLastY = e.clientY;
    minutesAccumPx = 0;
    minutesBaseValue = displayMinutes;
    minutesVelocity = 0;
    minutesLastMoveTime = performance.now();
    (e.currentTarget as HTMLElement).setPointerCapture(e.pointerId);
  }

  function handleMinutesPointerMove(e: PointerEvent) {
    if (!minutesDragging) return;
    e.preventDefault();
    const dy = minutesLastY - e.clientY;
    minutesAccumPx += dy;
    minutesLastY = e.clientY;

    const now = performance.now();
    const dt = now - minutesLastMoveTime;
    if (dt > 0) minutesVelocity = dy / dt;
    minutesLastMoveTime = now;

    const { newVal, fraction } = applyAccum("minutes", minutesAccumPx, minutesBaseValue);
    minutesFraction = fraction;
    if (newVal !== displayMinutes) {
      displayMinutes = newVal;
      emitValue(displayHours, newVal);
    }
  }

  function handleMinutesPointerUp(e: PointerEvent) {
    if (!minutesDragging) return;
    e.preventDefault();
    minutesDragging = false;

    const velocityPxPerFrame = minutesVelocity * 16;
    if (Math.abs(velocityPxPerFrame) > MIN_VELOCITY) {
      startMomentum("minutes", velocityPxPerFrame);
    } else {
      minutesFraction = 0;
      isAnimating = false;
    }
  }

  // ── Momentum animation ──

  function startMomentum(field: "hours" | "minutes", velocity: number) {
    stopMomentum();

    function tick() {
      velocity *= FRICTION;

      if (Math.abs(velocity) < MIN_VELOCITY) {
        // Snap to nearest value
        if (field === "hours") hoursFraction = 0;
        else minutesFraction = 0;
        isAnimating = false;
        momentumRaf = null;
        return;
      }

      if (field === "hours") {
        hoursAccumPx += velocity;
        const { newVal, fraction } = applyAccum("hours", hoursAccumPx, hoursBaseValue);
        hoursFraction = fraction;
        if (newVal !== displayHours) {
          displayHours = newVal;
          emitValue(newVal, displayMinutes);
        }
      } else {
        minutesAccumPx += velocity;
        const { newVal, fraction } = applyAccum("minutes", minutesAccumPx, minutesBaseValue);
        minutesFraction = fraction;
        if (newVal !== displayMinutes) {
          displayMinutes = newVal;
          emitValue(displayHours, newVal);
        }
      }

      momentumRaf = requestAnimationFrame(tick);
    }

    momentumRaf = requestAnimationFrame(tick);
  }

  function format(n: number): string {
    return String(n).padStart(2, "0");
  }
</script>

<div class="time-spinner" style="font-family: {countdownFont ? `'${countdownFont}', monospace` : 'monospace'};">

  <!-- Hours wheel -->
  <div
    class="wheel-field"
    class:dragging={hoursDragging}
    role="slider"
    aria-label="Hours"
    aria-valuemin={0}
    aria-valuemax={23}
    aria-valuenow={displayHours}
    tabindex={0}
    onpointerdown={handleHoursPointerDown}
    onpointermove={handleHoursPointerMove}
    onpointerup={handleHoursPointerUp}
    onpointercancel={handleHoursPointerUp}
  >
    <div class="wheel-viewport">
      <div class="wheel-cylinder">
        {#each hoursItems as item}
          <div
            class="wheel-item"
            style="
              transform: translateY(-50%) rotateX({-item.angle}deg) translateZ({WHEEL_RADIUS}px);
              opacity: {item.opacity};
            "
          >
            {format(item.value)}
          </div>
        {/each}
      </div>
    </div>
    <span class="unit-badge">h</span>
  </div>

  <span class="separator">:</span>

  <!-- Minutes wheel -->
  <div
    class="wheel-field"
    class:dragging={minutesDragging}
    role="slider"
    aria-label="Minutes"
    aria-valuemin={0}
    aria-valuemax={59}
    aria-valuenow={displayMinutes}
    tabindex={0}
    onpointerdown={handleMinutesPointerDown}
    onpointermove={handleMinutesPointerMove}
    onpointerup={handleMinutesPointerUp}
    onpointercancel={handleMinutesPointerUp}
  >
    <div class="wheel-viewport">
      <div class="wheel-cylinder">
        {#each minutesItems as item}
          <div
            class="wheel-item"
            style="
              transform: translateY(-50%) rotateX({-item.angle}deg) translateZ({WHEEL_RADIUS}px);
              opacity: {item.opacity};
            "
          >
            {format(item.value)}
          </div>
        {/each}
      </div>
    </div>
    <span class="unit-badge">m</span>
  </div>
</div>

<style>
  .time-spinner {
    display: inline-flex;
    align-items: center;
    gap: 4px;
    user-select: none;
    touch-action: none;
  }

  .wheel-field {
    position: relative;
    width: 50px;
    height: 36px;
    border-radius: 8px;
    background: rgba(255, 255, 255, 0.05);
    border: 1px solid rgba(255, 255, 255, 0.08);
    cursor: ns-resize;
    touch-action: none;
    overflow: hidden;
  }

  .wheel-field.dragging {
    background: rgba(255, 255, 255, 0.07);
    border-color: rgba(255, 255, 255, 0.18);
  }

  /* Perspective container — looking into the cylinder from outside */
  .wheel-viewport {
    width: 100%;
    height: 100%;
    perspective: 90px;
    display: flex;
    align-items: center;
    justify-content: center;
    overflow: hidden;
  }

  /* The 3D cylinder that holds number items */
  .wheel-cylinder {
    position: relative;
    width: 100%;
    height: 0;
    transform-style: preserve-3d;
  }

  /* Individual number on the cylinder surface */
  .wheel-item {
    position: absolute;
    width: 100%;
    text-align: center;
    font-size: 11px;
    font-weight: 600;
    color: rgba(255, 255, 255, 0.95);
    line-height: 1;
    backface-visibility: hidden;
    pointer-events: none;
    padding-right: 12px;
  }

  /* Unit label pinned to the right of the field */
  .unit-badge {
    position: absolute;
    right: 5px;
    top: 50%;
    transform: translateY(-50%);
    font-size: 10px;
    font-weight: 500;
    color: rgba(255, 255, 255, 0.3);
    pointer-events: none;
  }

  .separator {
    color: rgba(255, 255, 255, 0.25);
    font-weight: 500;
    user-select: none;
  }
</style>
