<script lang="ts">
  import {
    app,
    setMarkerTimes,
    setPixelsPerSecond,
    setScrollLeft,
    setSelectedIds,
    setTimelineWidth,
    snapToFrame,
    toggleSelection,
  } from '../store.svelte';
  import { ZOOM_MAX, ZOOM_MIN } from '../types';
  import {
    centerScrollOnPlayhead,
    formatTime,
    pxToTime,
    tickInterval,
    timeToPx,
  } from './math';

  const DRAG_THRESHOLD_PX = 4;

  interface Props {
    onSeek?: (time: number) => void;
    onReschedule?: () => void;
  }

  let { onSeek, onReschedule }: Props = $props();

  let viewportEl: HTMLDivElement | undefined = $state();
  let dragMode = $state<'none' | 'marker' | 'marquee' | 'move' | 'scrub'>('none');
  let pendingAction = $state<'none' | 'marker' | 'marquee' | 'scrub'>('none');
  let dragMarkerId = $state<string | null>(null);
  let pointerDownX = 0;
  let dragStartX = 0;
  let marqueeStartX = $state(0);
  let marqueeEndX = $state(0);
  let moveStartTimes = new Map<string, number>();
  let didDrag = false;
  let isScrubbing = false;
  let marqueeAdditive = false;
  let capturedPointerId: number | null = null;

  export function focus() {
    viewportEl?.focus({ preventScroll: true });
  }

  function updateScrollCenter() {
    setScrollLeft(
      centerScrollOnPlayhead(app.currentTime, app.pixelsPerSecond, app.timelineWidth),
    );
  }

  $effect(() => {
    void app.currentTime;
    void app.pixelsPerSecond;
    if (app.duration > 0 && viewportEl && !isScrubbing) updateScrollCenter();
  });

  $effect(() => {
    if (!viewportEl) return;
    const observer = new ResizeObserver(() => {
      setTimelineWidth(viewportEl!.clientWidth);
    });
    observer.observe(viewportEl);
    setTimelineWidth(viewportEl.clientWidth);
    return () => observer.disconnect();
  });

  function clampZoom(value: number) {
    return Math.max(ZOOM_MIN, Math.min(ZOOM_MAX, value));
  }

  function handleWheel(event: WheelEvent) {
    if (!event.altKey) return;
    event.preventDefault();
    const factor = event.deltaY < 0 ? 1.15 : 1 / 1.15;
    setPixelsPerSecond(clampZoom(app.pixelsPerSecond * factor));
    updateScrollCenter();
  }

  function localX(event: PointerEvent): number {
    if (!viewportEl) return 0;
    const rect = viewportEl.getBoundingClientRect();
    return event.clientX - rect.left + app.scrollLeft;
  }

  function timeAtX(x: number): number {
    return snapToFrame(
      Math.max(0, Math.min(app.duration || Infinity, pxToTime(x, app.pixelsPerSecond))),
    );
  }

  function scrubToX(x: number) {
    isScrubbing = true;
    onSeek?.(timeAtX(x));
  }

  function activateDragMode() {
    if (dragMode !== 'none' || pendingAction === 'none') return;

    if (pendingAction === 'marker' && dragMarkerId) {
      dragMode = app.selectedIds.has(dragMarkerId) ? 'move' : 'marker';
      return;
    }

    if (pendingAction === 'marquee') {
      dragMode = 'marquee';
      return;
    }

    if (pendingAction === 'scrub') {
      dragMode = 'scrub';
      isScrubbing = true;
    }
  }

  function handlePointerDown(event: PointerEvent) {
    if (!viewportEl) return;

    const x = localX(event);
    const target = event.target as HTMLElement;
    const markerEl = target.closest('[data-marker-id]') as HTMLElement | null;
    const inRuler = target.closest('.ruler');

    pointerDownX = x;
    dragStartX = x;
    didDrag = false;
    dragMode = 'none';

    // Middle-click drag: scrub playhead anywhere on the timeline
    if (event.button === 1) {
      event.preventDefault();
      pendingAction = 'scrub';
      dragMode = 'scrub';
      scrubToX(x);
      viewportEl.setPointerCapture(event.pointerId);
      capturedPointerId = event.pointerId;
      return;
    }

    if (event.button !== 0) return;

    if (markerEl?.dataset.markerId) {
      event.preventDefault();
      const id = markerEl.dataset.markerId;
      const wasSelected = app.selectedIds.has(id);
      if (event.shiftKey) {
        toggleSelection(id, true);
      } else if (!wasSelected) {
        setSelectedIds(new Set([id]));
      }
      pendingAction = 'marker';
      dragMarkerId = id;
      moveStartTimes = new Map(
        app.markers.filter((m) => app.selectedIds.has(m.id)).map((m) => [m.id, m.time]),
      );
      viewportEl.setPointerCapture(event.pointerId);
      capturedPointerId = event.pointerId;
      return;
    }

    if (inRuler) {
      pendingAction = 'scrub';
      viewportEl.setPointerCapture(event.pointerId);
      capturedPointerId = event.pointerId;
      return;
    }

    // Track empty area: box-select (shift adds to existing selection)
    marqueeAdditive = event.shiftKey;
    if (!event.shiftKey) setSelectedIds(new Set());
    pendingAction = 'marquee';
    marqueeStartX = x;
    marqueeEndX = x;
    viewportEl.setPointerCapture(event.pointerId);
    capturedPointerId = event.pointerId;
  }

  function handlePointerMove(event: PointerEvent) {
    if (pendingAction === 'none' || !viewportEl) return;
    const x = localX(event);

    if (dragMode === 'scrub') {
      scrubToX(x);
      return;
    }

    if (!didDrag && Math.abs(x - pointerDownX) >= DRAG_THRESHOLD_PX) {
      didDrag = true;
      activateDragMode();
    }

    if (dragMode === 'marquee') {
      marqueeEndX = x;
      return;
    }

    if (dragMode === 'move' && moveStartTimes.size > 0) {
      const deltaTime = pxToTime(x - dragStartX, app.pixelsPerSecond);
      const updates = new Map<string, number>();
      for (const [id, start] of moveStartTimes) {
        const next = Math.max(0, Math.min(app.duration || Infinity, start + deltaTime));
        updates.set(id, snapToFrame(next));
      }
      setMarkerTimes(updates);
      return;
    }

    if (dragMode === 'marker' && dragMarkerId) {
      setMarkerTimes(new Map([[dragMarkerId, timeAtX(x)]]));
    }
  }

  function finishPointerInteraction(event?: PointerEvent) {
    if (pendingAction === 'none') return;

    const x = event ? localX(event) : pointerDownX;
    const wasScrubbing = isScrubbing;

    if (dragMode === 'marquee') {
      const minX = Math.min(marqueeStartX, marqueeEndX);
      const maxX = Math.max(marqueeStartX, marqueeEndX);
      const minT = pxToTime(minX, app.pixelsPerSecond);
      const maxT = pxToTime(maxX, app.pixelsPerSecond);
      const hits = app.markers.filter((m) => m.time >= minT && m.time <= maxT).map((m) => m.id);
      if (marqueeAdditive) {
        const next = new Set(app.selectedIds);
        for (const id of hits) next.add(id);
        setSelectedIds(next);
      } else {
        setSelectedIds(new Set(hits));
      }
    } else if (dragMode === 'none' && pendingAction === 'scrub' && !didDrag) {
      onSeek?.(timeAtX(x));
    }

    if (dragMode === 'move' || dragMode === 'marker') {
      onReschedule?.();
    }

    if (dragMode === 'scrub' || wasScrubbing) {
      onReschedule?.();
    }

    dragMode = 'none';
    pendingAction = 'none';
    dragMarkerId = null;
    moveStartTimes.clear();
    didDrag = false;
    isScrubbing = false;
    marqueeAdditive = false;

    if (viewportEl && capturedPointerId !== null && event) {
      viewportEl.releasePointerCapture(capturedPointerId);
    }
    capturedPointerId = null;

    if (wasScrubbing && app.duration > 0) {
      updateScrollCenter();
    }
  }

  function handlePointerUp(event: PointerEvent) {
    finishPointerInteraction(event);
  }

  function handlePointerCancel(event: PointerEvent) {
    finishPointerInteraction(event);
  }

  function handleLostPointerCapture() {
    if (pendingAction !== 'none') finishPointerInteraction();
  }

  function handleContextMenu(event: MouseEvent) {
    if (event.button === 1 || isScrubbing) event.preventDefault();
  }

  function handleZoomInput(event: Event) {
    const value = Number((event.currentTarget as HTMLInputElement).value);
    setPixelsPerSecond(value);
    updateScrollCenter();
  }

  const contentWidth = $derived(
    Math.max(app.timelineWidth, timeToPx(app.duration || 1, app.pixelsPerSecond) + 40),
  );
  const ticks = $derived.by(() => {
    const interval = tickInterval(app.pixelsPerSecond);
    const count = Math.ceil((app.duration || 1) / interval);
    return Array.from({ length: count + 1 }, (_, i) => i * interval);
  });
  const playheadX = $derived(timeToPx(app.currentTime, app.pixelsPerSecond));
  const marqueeVisible = $derived(dragMode === 'marquee');
  const marqueeLeft = $derived(Math.min(marqueeStartX, marqueeEndX));
  const marqueeWidth = $derived(Math.abs(marqueeEndX - marqueeStartX));
</script>

<div class="timeline-panel">
  <div class="zoom-row">
    <label for="zoom-slider">Zoom</label>
    <input
      id="zoom-slider"
      type="range"
      min={ZOOM_MIN}
      max={ZOOM_MAX}
      value={app.pixelsPerSecond}
      oninput={handleZoomInput}
    />
    <span class="hint">Alt + scroll to zoom</span>
  </div>

  <!-- svelte-ignore a11y_no_noninteractive_tabindex -->
  <!-- svelte-ignore a11y_no_noninteractive_element_interactions -->
  <!-- svelte-ignore a11y_no_static_element_interactions -->
  <div
    class="timeline-viewport"
    bind:this={viewportEl}
    tabindex="-1"
    onwheel={handleWheel}
    onpointerdown={handlePointerDown}
    onpointermove={handlePointerMove}
    onpointerup={handlePointerUp}
    onpointercancel={handlePointerCancel}
    onlostpointercapture={handleLostPointerCapture}
    oncontextmenu={handleContextMenu}
  >
    <div
      class="timeline-content"
      style="width: {contentWidth}px; transform: translateX({-app.scrollLeft}px);"
    >
      <div class="ruler">
        {#each ticks as tick}
          <div class="tick" style="left: {timeToPx(tick, app.pixelsPerSecond)}px">
            <span>{formatTime(tick)}</span>
          </div>
        {/each}
      </div>

      <div class="track">
        {#each app.markers as marker (marker.id)}
          <button
            type="button"
            class="marker"
            class:selected={app.selectedIds.has(marker.id)}
            tabindex="-1"
            data-marker-id={marker.id}
            style="left: {timeToPx(marker.time, app.pixelsPerSecond)}px"
            title="{marker.key} @ {formatTime(marker.time)}"
          >
            {marker.key}
          </button>
        {/each}

        {#if marqueeVisible}
          <div class="marquee" style="left: {marqueeLeft}px; width: {marqueeWidth}px"></div>
        {/if}

        <div class="playhead" style="left: {playheadX}px"></div>
      </div>
    </div>
  </div>
</div>

<style>
  .timeline-panel {
    display: flex;
    flex-direction: column;
    gap: 0.5rem;
    border-top: 1px solid #333;
    padding-top: 0.75rem;
  }

  .zoom-row {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    font-size: 0.85rem;
    color: #aaa;
  }

  .zoom-row input[type='range'] {
    flex: 1;
    max-width: 240px;
  }

  .hint {
    margin-left: auto;
  }

  .timeline-viewport {
    position: relative;
    overflow: hidden;
    height: 120px;
    background: #1a1a1a;
    border: 1px solid #333;
    border-radius: 6px;
    user-select: none;
    touch-action: none;
    outline: none;
  }

  .timeline-viewport:focus-visible {
    border-color: #63b3ed;
    box-shadow: 0 0 0 1px #63b3ed;
  }

  .timeline-content {
    position: relative;
    height: 100%;
    will-change: transform;
  }

  .ruler {
    position: relative;
    height: 28px;
    border-bottom: 1px solid #333;
    background: #222;
    cursor: ew-resize;
  }

  .tick {
    position: absolute;
    top: 0;
    height: 100%;
    border-left: 1px solid #444;
    padding-left: 4px;
    font-size: 0.65rem;
    color: #888;
    pointer-events: none;
  }

  .track {
    position: relative;
    height: calc(100% - 28px);
    cursor: crosshair;
  }

  .marker {
    position: absolute;
    top: 18px;
    transform: translateX(-50%);
    padding: 0.2rem 0.45rem;
    border: 1px solid #555;
    border-radius: 4px;
    background: #2d3748;
    color: #e2e8f0;
    font-size: 0.75rem;
    cursor: grab;
    white-space: nowrap;
  }

  .marker.selected {
    background: #3182ce;
    border-color: #63b3ed;
    box-shadow: 0 0 0 1px #63b3ed;
  }

  .marquee {
    position: absolute;
    top: 8px;
    height: 72px;
    background: rgba(99, 179, 237, 0.2);
    border: 1px solid #63b3ed;
    pointer-events: none;
  }

  .playhead {
    position: absolute;
    top: 0;
    bottom: 0;
    width: 2px;
    transform: translateX(-50%);
    background: #f56565;
    pointer-events: none;
    z-index: 2;
  }
</style>
