<script lang="ts">
  /**
   * HandwriteCanvas — Apple Pencil + 일반 터치/마우스 손글씨 캔버스.
   *
   * 핵심 (사용자 강조):
   *   - PointerEvent 만 사용. pointerType === 'pen' 검사 (palm rejection)
   *   - e.pressure (0~1, Pencil) — simulatePressure: true 로 안전망 (속도 기반)
   *   - touch-action: none (캔버스 한정)
   *   - devicePixelRatio 반영 (retina 선명)
   *   - stroke 단위 undo/redo
   *   - 5초 idle 또는 stroke 5개마다 onChange — 부모가 PATCH 트리거
   *   - 마운트 시 strokes_json 으로 전체 redraw (세션 복원)
   *   - localStorage 백업 (네트워크 단절 대비)
   *
   * Tool:
   *   - 'pen' (default): 새 stroke 그리기
   *   - 'eraser': pointer 가 지나가는 stroke 를 hit-test 로 즉시 삭제
   *     (전체 지우기 버튼은 별도)
   */
  import { onMount, onDestroy } from 'svelte';
  import { getStroke } from 'perfect-freehand';
  import { Eraser, Pencil, Undo2, Redo2, Trash2 } from 'lucide-svelte';
  import IconButton from '$lib/components/ui/IconButton.svelte';

  type Point = [number, number, number]; // [x, y, pressure]
  type Stroke = {
    id: string;
    points: Point[];
  };
  export type StrokesJson = {
    version: 1;
    strokes: Stroke[];
  };
  type Tool = 'pen' | 'eraser';

  interface Props {
    sessionId: number;
    initialStrokes?: StrokesJson | null;
    /** 트레이싱 모드 — 캔버스 배경에 회색으로 깔리는 텍스트. */
    traceText?: string | null;
    /** 펜 기본 굵기 (default 6). kanji 면 약간 굵게 권장. */
    baseSize?: number;
    /** PATCH /api/study-sessions/{id} strokes_json 트리거. */
    onChange?: (strokes: StrokesJson) => void;
    /** POST /snapshot — Blob 전달. */
    onSnapshot?: (blob: Blob) => Promise<void>;
  }

  let {
    sessionId,
    initialStrokes = null,
    traceText = null,
    baseSize = 6,
    onChange,
    onSnapshot,
  }: Props = $props();

  // ── 상태 ──
  let canvas = $state<HTMLCanvasElement | null>(null);
  let containerEl = $state<HTMLDivElement | null>(null);
  let cssWidth = $state(800);
  let cssHeight = $state(600);

  let strokes = $state<Stroke[]>(initialStrokes?.strokes ?? []);
  let undoStack = $state<Stroke[]>([]); // redo 큐 (clear 또는 undo 로 빠진 stroke)
  let inflight: Stroke | null = $state(null);
  let activePointerId: number | null = null;
  let strokeColor = $state('#e4e4e7'); // 마운트 시 --text 토큰 실측으로 갱신
  let tool = $state<Tool>('pen');

  let isDirty = $state(false);
  let saveTimer: number | null = null;
  let snapshotting = $state(false);
  let snapshotErr = $state<string | null>(null);

  // 굵기 단계 (사용자 선택). baseSize 가 기본, multiplier 곱해서 적용.
  type Width = 'thin' | 'normal' | 'thick';
  let widthMode = $state<Width>('normal');
  const WIDTH_FACTOR: Record<Width, number> = { thin: 0.6, normal: 1, thick: 1.6 };
  let effectiveSize = $derived(baseSize * WIDTH_FACTOR[widthMode]);

  // 지우개 반경 (CSS px) — effectiveSize 의 약 4배. Apple Pencil 정밀도와 균형.
  let eraserRadius = $derived(Math.max(16, effectiveSize * 4));

  // ── localStorage backup ──
  const lsKey = $derived(`study_session_${sessionId}_strokes`);
  function backupToLocalStorage() {
    try {
      localStorage.setItem(lsKey, JSON.stringify({ version: 1, strokes }));
    } catch {
      // quota / 비활성 — 무시
    }
  }
  function restoreFromLocalStorageIfNewer() {
    try {
      const raw = localStorage.getItem(lsKey);
      if (!raw) return;
      const parsed = JSON.parse(raw) as StrokesJson;
      if ((initialStrokes?.strokes?.length ?? 0) === 0 && parsed.strokes.length > 0) {
        strokes = parsed.strokes;
        scheduleSave();
      }
    } catch {
      // 깨진 데이터 — 무시
    }
  }

  // ── DPR + resize ──
  function resizeCanvas() {
    if (!canvas || !containerEl) return;
    const rect = containerEl.getBoundingClientRect();
    cssWidth = Math.max(200, Math.floor(rect.width));
    cssHeight = Math.max(200, Math.floor(rect.height));
    const dpr = window.devicePixelRatio || 1;
    canvas.width = cssWidth * dpr;
    canvas.height = cssHeight * dpr;
    canvas.style.width = `${cssWidth}px`;
    canvas.style.height = `${cssHeight}px`;
    const ctx = canvas.getContext('2d');
    if (ctx) ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
    redraw();
  }

  // ── render — perfect-freehand polygon outline + bezier 보간으로 Notability 수준 필기감.
  // 알고리즘:
  //   1) getStroke() → 압력/속도 기반 polygon outline points
  //   2) outline 을 quadratic bezier 로 연결 → 부드러운 곡선
  //   3) ctx.fill() — anti-aliased polygon
  function drawStroke(ctx: CanvasRenderingContext2D, s: Stroke) {
    if (s.points.length === 0) return;

    const outline = getStroke(s.points, {
      size: effectiveSize,
      thinning: 0.6,        // 압력 변화 폭. 강할수록 압력 차이 진함.
      smoothing: 0.65,      // 점 보간 강도
      streamline: 0.5,      // stroke 안정화 (떨림 감소)
      simulatePressure: false, // 실제 e.pressure 사용
      last: true,
      easing: (t) => t,
    });
    if (outline.length < 2) return;

    ctx.fillStyle = strokeColor;
    ctx.beginPath();
    ctx.moveTo(outline[0][0], outline[0][1]);
    for (let i = 1; i < outline.length - 1; i++) {
      const [x1, y1] = outline[i];
      const [x2, y2] = outline[i + 1];
      const mx = (x1 + x2) / 2;
      const my = (y1 + y2) / 2;
      ctx.quadraticCurveTo(x1, y1, mx, my);
    }
    const last = outline[outline.length - 1];
    ctx.lineTo(last[0], last[1]);
    ctx.closePath();
    ctx.fill();
  }

  function drawTraceBackground(ctx: CanvasRenderingContext2D) {
    if (!traceText) return;
    ctx.save();
    ctx.fillStyle = 'rgba(120, 120, 120, 0.18)';
    const fontSize = Math.min(cssWidth, cssHeight) * 0.4;
    ctx.font = `${fontSize}px sans-serif`;
    ctx.textAlign = 'center';
    ctx.textBaseline = 'middle';
    ctx.fillText(traceText, cssWidth / 2, cssHeight / 2);
    ctx.restore();
  }

  function redraw() {
    if (!canvas) return;
    const ctx = canvas.getContext('2d');
    if (!ctx) return;
    ctx.clearRect(0, 0, cssWidth, cssHeight);
    drawTraceBackground(ctx);
    for (const s of strokes) {
      drawStroke(ctx, s);
    }
    if (inflight) {
      drawStroke(ctx, inflight);
    }
  }

  // ── pointer 헬퍼 ──
  function getLocalXY(e: PointerEvent): [number, number] {
    if (!canvas) return [0, 0];
    const rect = canvas.getBoundingClientRect();
    // canvas 의 실제 표시 크기와 attribute 크기가 다른 경우 (ResizeObserver 가 아직 못 따라잡은 케이스)
    // 비율로 보정. rect.width 가 canvas 의 실제 표시 width.
    const scaleX = rect.width === 0 ? 1 : cssWidth / rect.width;
    const scaleY = rect.height === 0 ? 1 : cssHeight / rect.height;
    return [
      (e.clientX - rect.left) * scaleX,
      (e.clientY - rect.top) * scaleY,
    ];
  }

  function isPenLike(e: PointerEvent): boolean {
    // 사용자 요청: Apple Pencil ('pen') 만 인식. 손가락 ('touch') 거부 — palm rejection.
    // 데스크톱 테스트용 'mouse' 는 허용.
    return e.pointerType === 'pen' || e.pointerType === 'mouse';
  }

  // ── 지우개 hit-test ──
  /** 주어진 좌표 (x, y) 반경 안에 있는 stroke 들을 strokes 배열에서 제거. */
  function eraseAt(x: number, y: number): boolean {
    if (strokes.length === 0) return false;
    const r2 = eraserRadius * eraserRadius;
    const removed: Stroke[] = [];
    const keep: Stroke[] = [];
    for (const s of strokes) {
      let hit = false;
      // 점-원 hit. 점 사이 라인 hit-test 까진 안 함 (Phase 1 단순화 — 보통 stroke 간격이 촘촘).
      for (const [px, py] of s.points) {
        const dx = px - x;
        const dy = py - y;
        if (dx * dx + dy * dy <= r2) { hit = true; break; }
      }
      (hit ? removed : keep).push(s);
    }
    if (removed.length === 0) return false;
    strokes = keep;
    undoStack = [...undoStack, ...removed];
    return true;
  }

  // ── pointer 핸들러 ──
  function onPointerDown(e: PointerEvent) {
    if (!canvas || !isPenLike(e)) return;
    e.preventDefault();

    // 이전 stroke 의 inflight 가 어떤 이유로 살아있다면 강제 종료 (완성된 stroke 로 보존).
    // 빠른 연속 입력 시 두번째 stroke 가 누락되는 race 방어.
    if (inflight) {
      if (inflight.points.length > 1) {
        strokes = [...strokes, inflight];
        isDirty = true;
        backupToLocalStorage();
      }
      inflight = null;
    }

    activePointerId = e.pointerId;
    const [x, y] = getLocalXY(e);

    if (tool === 'eraser') {
      const removed = eraseAt(x, y);
      if (removed) {
        isDirty = true;
        backupToLocalStorage();
        scheduleSave();
        redraw();
      }
      return;
    }

    inflight = {
      id: `s_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`,
      points: [[x, y, e.pressure || 0.5]],
    };
    redraw();
  }

  function onPointerMove(e: PointerEvent) {
    if (e.pointerId !== activePointerId) return;
    const [x, y] = getLocalXY(e);

    if (tool === 'eraser') {
      const removed = eraseAt(x, y);
      if (removed) {
        isDirty = true;
        backupToLocalStorage();
        scheduleSave();
        redraw();
      }
      return;
    }

    if (!inflight) return;
    // coalesced events 가 있으면 보간으로만 사용. main 점은 항상 push 보장.
    const coalesced = (e.getCoalescedEvents?.() ?? []) as PointerEvent[];
    for (const ev of coalesced) {
      const [cx, cy] = getLocalXY(ev);
      inflight.points.push([cx, cy, ev.pressure || 0.5]);
    }
    inflight.points.push([x, y, e.pressure || 0.5]);
    redraw();
  }

  function endStroke(e: PointerEvent) {
    // pointerleave 가 stale pointerId 로 들어와 새 stroke 를 강제 종료하던 race 방어:
    // inflight 가 없으면 (첫 stroke 이미 끝났음) 무시.
    if (!inflight) {
      if (e.pointerId === activePointerId) activePointerId = null;
      return;
    }
    if (e.pointerId !== activePointerId) return;
    activePointerId = null;

    if (tool === 'eraser') return;

    if (inflight.points.length > 1) {
      strokes = [...strokes, inflight];
      undoStack = [];
      isDirty = true;
      backupToLocalStorage();
      scheduleSave();
    }
    inflight = null;
    redraw();
  }

  // ── undo/redo/clear ──
  function undo() {
    if (strokes.length === 0) return;
    const last = strokes[strokes.length - 1];
    strokes = strokes.slice(0, -1);
    undoStack = [...undoStack, last];
    isDirty = true;
    backupToLocalStorage();
    scheduleSave();
    redraw();
  }
  function redo() {
    if (undoStack.length === 0) return;
    const last = undoStack[undoStack.length - 1];
    undoStack = undoStack.slice(0, -1);
    strokes = [...strokes, last];
    isDirty = true;
    backupToLocalStorage();
    scheduleSave();
    redraw();
  }
  function clearAll() {
    if (strokes.length === 0) return;
    if (!confirm('이 세션의 모든 stroke 를 지웁니다. 계속할까요?')) return;
    undoStack = [...undoStack, ...strokes];
    strokes = [];
    isDirty = true;
    backupToLocalStorage();
    scheduleSave();
    redraw();
  }

  // ── 자동 저장 디바운스 ──
  function scheduleSave() {
    if (saveTimer) clearTimeout(saveTimer);
    saveTimer = window.setTimeout(flushSave, 5000) as unknown as number;
    if (strokes.length > 0 && strokes.length % 5 === 0) {
      flushSave();
    }
  }
  function flushSave() {
    if (saveTimer) {
      clearTimeout(saveTimer);
      saveTimer = null;
    }
    if (!isDirty) return;
    isDirty = false;
    onChange?.({ version: 1, strokes });
  }

  // ── snapshot (PNG) ──
  async function takeSnapshot() {
    if (!canvas || !onSnapshot) return;
    if (snapshotting) return;
    snapshotting = true;
    snapshotErr = null;
    try {
      redraw();
      flushSave();
      const blob: Blob | null = await new Promise((resolve) =>
        canvas!.toBlob((b) => resolve(b), 'image/png')
      );
      if (!blob) throw new Error('PNG 생성 실패');
      await onSnapshot(blob);
    } catch (e) {
      snapshotErr = (e as { detail?: string; message?: string }).detail
        || (e as Error).message
        || '스냅샷 저장 실패';
    } finally {
      snapshotting = false;
    }
  }

  function onBeforeUnload() {
    if (isDirty) flushSave();
  }

  // ── 마운트 ──
  let resizeObserver: ResizeObserver | null = null;
  function onWindowResize() { resizeCanvas(); }
  onMount(() => {
    try {
      const txt = getComputedStyle(document.documentElement).getPropertyValue('--text').trim();
      if (txt) strokeColor = txt;
    } catch {
      // SSR / 비표준 환경 — fallback 유지
    }
    resizeCanvas();
    restoreFromLocalStorageIfNewer();
    // layout 이 안정된 후 한 번 더 (flex 레이아웃 첫 paint 직후 0x0 케이스 방어)
    requestAnimationFrame(() => requestAnimationFrame(resizeCanvas));
    redraw();

    if (containerEl && 'ResizeObserver' in window) {
      resizeObserver = new ResizeObserver(() => resizeCanvas());
      resizeObserver.observe(containerEl);
    }
    window.addEventListener('resize', onWindowResize);
    window.addEventListener('orientationchange', onWindowResize);
    window.addEventListener('beforeunload', onBeforeUnload);
  });
  onDestroy(() => {
    flushSave();
    resizeObserver?.disconnect();
    if (typeof window !== 'undefined') {
      window.removeEventListener('beforeunload', onBeforeUnload);
      window.removeEventListener('resize', onWindowResize);
      window.removeEventListener('orientationchange', onWindowResize);
    }
  });

  // initialStrokes 동기화는 onMount 한 번만 (위에서 이미 처리). $effect 사용 시
  // strokes 도 의존성으로 추적되어 사용자가 stroke 추가하는 순간 옛 initialStrokes 로
  // 되돌아가는 버그 발생 → 펜 떼면 새 stroke 사라짐.
  // 부모가 prop 으로 새 initialStrokes 를 보내도 무시 (사용자 진행 stroke 우선).

  // traceText 변경 시만 redraw 트리거 (의존성 명시 access).
  let _prevTraceText = $state(traceText);
  $effect(() => {
    if (traceText !== _prevTraceText) {
      _prevTraceText = traceText;
      redraw();
    }
  });
</script>

<div class="flex flex-col h-full" style="touch-action: manipulation;">
  <!-- 툴바 — touch-action: manipulation 으로 더블탭 줌 차단 -->
  <div class="flex items-center gap-1 px-2 py-1 border-b border-default bg-surface shrink-0 flex-wrap" style="touch-action: manipulation;">
    <!-- Pen / Eraser 모드 토글 -->
    <button
      type="button"
      onclick={() => (tool = 'pen')}
      class="flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors
        {tool === 'pen' ? 'bg-accent/15 text-accent' : 'text-dim hover:bg-surface'}"
      aria-pressed={tool === 'pen'}
    >
      <Pencil size={14} /> 펜
    </button>
    <button
      type="button"
      onclick={() => (tool = 'eraser')}
      class="flex items-center gap-1 px-2 py-1 rounded text-xs transition-colors
        {tool === 'eraser' ? 'bg-accent/15 text-accent' : 'text-dim hover:bg-surface'}"
      aria-pressed={tool === 'eraser'}
    >
      <Eraser size={14} /> 지우개
    </button>

    <span class="w-px h-5 bg-default mx-1" aria-hidden></span>

    <!-- 굵기 선택 — 압력은 자동 반영, 이건 base 굵기 -->
    {#each [['thin', '가늘게'], ['normal', '보통'], ['thick', '굵게']] as [w, label]}
      <button
        type="button"
        onclick={() => (widthMode = w)}
        class="px-2 py-1 rounded text-xs transition-colors
          {widthMode === w ? 'bg-accent/15 text-accent' : 'text-dim hover:bg-surface'}"
        aria-pressed={widthMode === w}
      >{label}</button>
    {/each}

    <span class="w-px h-5 bg-default mx-1" aria-hidden></span>

    <IconButton icon={Undo2} size="sm" aria-label="실행 취소" onclick={undo} disabled={strokes.length === 0} />
    <IconButton icon={Redo2} size="sm" aria-label="다시 실행" onclick={redo} disabled={undoStack.length === 0} />
    <IconButton icon={Trash2} size="sm" aria-label="모두 지우기" onclick={clearAll} disabled={strokes.length === 0} />

    <span class="text-xs text-dim ml-2">stroke {strokes.length}</span>
    <div class="flex-1"></div>
    {#if snapshotErr}
      <span class="text-xs text-error mr-2">{snapshotErr}</span>
    {/if}
    <button
      type="button"
      onclick={takeSnapshot}
      disabled={snapshotting || strokes.length === 0}
      class="px-3 py-1 rounded text-sm bg-accent text-white disabled:opacity-50 disabled:cursor-not-allowed"
    >
      {snapshotting ? '저장 중...' : 'PNG 저장'}
    </button>
  </div>

  <!-- 캔버스 컨테이너 — 명확한 border 로 영역 구분 -->
  <div bind:this={containerEl} class="flex-1 min-h-0 bg-bg relative overflow-hidden border-2 border-default/30">
    <canvas
      bind:this={canvas}
      onpointerdown={onPointerDown}
      onpointermove={onPointerMove}
      onpointerup={endStroke}
      onpointercancel={endStroke}
      onpointerleave={(e) => { if (inflight && e.pointerId === activePointerId) endStroke(e); }}
      class="block"
      style="touch-action: none; cursor: {tool === 'eraser' ? 'cell' : 'crosshair'};"
    ></canvas>

  </div>
</div>
