/**
 * 지리정보 오버레이
 * 렌더링 최적화:
 *   - 텍스트(측점+POI): 데이터 로드 완료 시 전 프레임 Map 사전 계산 (requestIdleCallback)
 *     params 변경 시 500ms debounce 후 재계산
 *   - 중심선: 드론 프레임 변경 시 renderCacheRef 갱신 (per-frame, 나중에 최적화)
 *   - RAF 루프: Map 조회 + 캐시 읽기만 (계산 없음 → 60fps)
 */

import React, { useEffect, useRef, useState, useCallback } from 'react';
import {
  toCameraCoords,
  pixelFromCamera,
  type DroneFrameBasic,
  type CameraParams,
  type CameraCoords,
  DEFAULT_CAMERA_PARAMS,
} from '../../utils/geoProjection';

interface GeoPoint {
  title: string;
  category: string;
  lat: number;
  lon: number;
  z: number;
  type: 'poi' | 'station';
}

interface CenterlinePoint {
  lat: number;
  lon: number;
  z: number;
}

const VIDEO_FPS = 30000 / 1001;

interface Props {
  currentFrame: number;
  currentTime: number;
  fps: number;
  visible: boolean;
}

// category → 이모지
const CATEGORY_EMOJI: Record<string, string> = {
  '터널': '🚇',
  '교량': '🌉',
  '역사': '🚉',
  '지장물': '🏢',
  '측점': '📍',
};

// 텍스트 사전 계산 캐시 (Map<frameNum, LabelCache>)
interface LabelCache {
  stationLabels: { sx: number; sy: number; title: string }[];
  poiMarkers: { x: number; y: number; title: string; category: string }[];
}

// 중심선 + 나침반 렌더 캐시 (per-frame, renderCacheRef)
interface RenderCache {
  centerlineSegs: [number, number, number, number][];
  effectiveYaw: number;
  hFovRad: number;
  clCount: number;
  poiCount: number;
}

const cleanTitle = (t: string) => t.replace(/\s*\([상하]\)\s*$/, '').trim();

function stationOrder(title: string): number {
  const m = title.match(/(\d+)[Kk](\d+)/);
  if (!m) return 0;
  return parseInt(m[1]) * 1000 + parseInt(m[2]);
}

// ── ParamRow ─────────────────────────────────────────────────────────────────

interface ParamRowProps {
  label: string; value: number; min: number; max: number; step: number;
  unit: string; decimals?: number; onChange: (v: number) => void;
}

function ParamRow({ label, value, min, max, step, unit, decimals = 1, onChange }: ParamRowProps) {
  const fmt = useCallback((v: number) => v.toFixed(decimals), [decimals]);
  const [text, setText] = useState(() => fmt(value));
  const prevRef = useRef(value);
  useEffect(() => {
    if (prevRef.current !== value) { prevRef.current = value; setText(fmt(value)); }
  }, [value, fmt]);
  const commit = (s: string) => {
    const n = parseFloat(s);
    if (!isNaN(n)) {
      const c = Math.max(min, Math.min(max, n));
      onChange(c); setText(fmt(c)); prevRef.current = c;
    } else setText(fmt(value));
  };
  return (
    <div className="flex items-center gap-1.5 text-[11px]">
      <span className="text-gray-400 w-12 shrink-0 text-right">{label}</span>
      <input type="range" min={min} max={max} step={step} value={value}
        onChange={e => { const v = parseFloat(e.target.value); onChange(v); prevRef.current = v; setText(fmt(v)); }}
        className="flex-1 h-1 accent-yellow-400 cursor-pointer" />
      <input type="number" min={min} max={max} step={step} value={text}
        onChange={e => setText(e.target.value)}
        onBlur={e => commit(e.target.value)}
        onKeyDown={e => { if (e.key === 'Enter') commit((e.target as HTMLInputElement).value); if (e.key === 'Escape') setText(fmt(value)); }}
        className="w-16 bg-black/60 border border-gray-700 rounded px-1 py-0.5 text-right font-mono text-yellow-300 text-[11px] [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" />
      <span className="text-gray-500 text-[10px] w-5 shrink-0">{unit}</span>
    </div>
  );
}

// ── 메인 컴포넌트 ─────────────────────────────────────────────────────────────

export default function StationOverlay({ currentFrame, currentTime, fps, visible }: Props) {
  const canvasRef = useRef<HTMLCanvasElement>(null);
  const canvasSizeRef = useRef({ w: 0, h: 0 });

  // 데이터 ref
  const allDroneFramesRef = useRef<DroneFrameBasic[]>([]);
  const allCenterlinePointsRef = useRef<CenterlinePoint[]>([]);
  const allGeoStationsRef = useRef<GeoPoint[]>([]);
  const allPoisRef = useRef<GeoPoint[]>([]);

  // 현재 상태 ref
  const currentDroneFrameRef = useRef<DroneFrameBasic | null>(null);
  const currentFrameNumRef = useRef<number>(0);       // RAF에서 Map 조회용
  const currentFrameIdxRef = useRef<number>(0);       // smoothFrame용 배열 인덱스
  const currentTimeSecRef = useRef<number>(0);        // 마지막으로 알려진 재생 시간
  const timeUpdateWallRef = useRef<number>(performance.now()); // currentTime 갱신된 시각
  const paramsRef = useRef<CameraParams>(DEFAULT_CAMERA_PARAMS);
  const visibleRef = useRef(visible);
  const worldOriginRef = useRef<{ lat: number; lon: number; alt: number } | undefined>(undefined);

  // 텍스트 사전 계산 Map
  const labelMapRef = useRef<Map<number, LabelCache>>(new Map());
  const precomputeIdRef = useRef(0);  // 진행 중 계산 취소용

  // 중심선 + 나침반 렌더 캐시 (per-frame)
  const renderCacheRef = useRef<RenderCache | null>(null);

  // UI state
  const [params, setParams] = useState<CameraParams>(DEFAULT_CAMERA_PARAMS);
  const [smoothHalf, setSmoothHalf] = useState(10);
  const smoothHalfRef = useRef(10);
  const [emaAlpha, setEmaAlpha] = useState(0.01);
  const emaAlphaRef = useRef(0.01);
  // 표시 위치 EMA 상태 (RAF 내부 유지)
  const displayedStRef = useRef<Map<string, { x: number; y: number }>>(new Map());
  const displayedPoiRef = useRef<Map<string, { x: number; y: number }>>(new Map());
  const [showControls, setShowControls] = useState(false);
  const [droneFramesLoaded, setDroneFramesLoaded] = useState(false);
  const [geoDataLoaded, setGeoDataLoaded] = useState(false);
  const [clDataLoaded, setClDataLoaded] = useState(false);
  const [panelDroneFrame, setPanelDroneFrame] = useState<DroneFrameBasic | null>(null);

  useEffect(() => { paramsRef.current = params; }, [params]);
  useEffect(() => { visibleRef.current = visible; }, [visible]);
  useEffect(() => { smoothHalfRef.current = smoothHalf; }, [smoothHalf]);
  useEffect(() => { emaAlphaRef.current = emaAlpha; }, [emaAlpha]);

  const setParam = useCallback(<K extends keyof CameraParams>(key: K, val: CameraParams[K]) =>
    setParams(prev => ({ ...prev, [key]: val })), []);

  const nearestCL = useCallback((lat: number, lon: number): CenterlinePoint | null => {
    const pts = allCenterlinePointsRef.current;
    if (!pts.length) return null;
    let best = pts[0], bestD = (best.lat - lat) ** 2 + (best.lon - lon) ** 2;
    for (const pt of pts) {
      const d = (pt.lat - lat) ** 2 + (pt.lon - lon) ** 2;
      if (d < bestD) { bestD = d; best = pt; }
    }
    return best;
  }, []);

  function updateWorldOrigin() {
    const st = allGeoStationsRef.current;
    const cl = allCenterlinePointsRef.current;
    if (st[0]) worldOriginRef.current = { lat: st[0].lat, lon: st[0].lon, alt: st[0].z };
    else if (cl[0]) worldOriginRef.current = { lat: cl[0].lat, lon: cl[0].lon, alt: cl[0].z };
  }

  // 데이터 로드
  useEffect(() => {
    if (!visible) return;
    fetch('/api/geo/pois').then(r => r.json()).then((data: GeoPoint[]) => {
      allGeoStationsRef.current = (data || []).filter(p => p.type === 'station')
        .sort((a, b) => stationOrder(a.title) - stationOrder(b.title));
      allPoisRef.current = (data || []).filter(p => p.type === 'poi');
      updateWorldOrigin();
      setGeoDataLoaded(true);
    }).catch(() => {});
  }, [visible]);

  useEffect(() => {
    if (!visible) return;
    fetch('/api/geo/centerline').then(r => r.json()).then((data: CenterlinePoint[]) => {
      allCenterlinePointsRef.current = Array.isArray(data) ? data : [];
      updateWorldOrigin();
      setClDataLoaded(true);
    }).catch(() => {});
  }, [visible]);

  useEffect(() => {
    if (!visible || droneFramesLoaded) return;
    fetch('/api/geo/frames?step=1').then(r => r.json()).then((data: DroneFrameBasic[]) => {
      if (Array.isArray(data) && data.length > 0) {
        allDroneFramesRef.current = data;
        setDroneFramesLoaded(true);
      }
    }).catch(() => {});
  }, [visible, droneFramesLoaded]);

  // 드론 프레임 이동 평균 (GPS/자세 노이즈 제거)
  const smoothFrame = useCallback((frames: DroneFrameBasic[], i: number, halfWin: number): DroneFrameBasic => {
    const lo = Math.max(0, i - halfWin);
    const hi = Math.min(frames.length - 1, i + halfWin);
    const n = hi - lo + 1;
    let lat = 0, lon = 0, altitude = 0, pitch = 0, roll = 0, sinYaw = 0, cosYaw = 0;
    for (let k = lo; k <= hi; k++) {
      const f = frames[k];
      lat += f.lat; lon += f.lon; altitude += f.altitude;
      pitch += f.pitch; roll += f.roll;
      const yr = f.yaw * Math.PI / 180;
      sinYaw += Math.sin(yr); cosYaw += Math.cos(yr);
    }
    return {
      ...frames[i],
      lat: lat / n, lon: lon / n, altitude: altitude / n,
      pitch: pitch / n, roll: roll / n,
      yaw: Math.atan2(sinYaw / n, cosYaw / n) * 180 / Math.PI,
    };
  }, []);

  // 텍스트 사전 계산 — requestIdleCallback으로 백그라운드 실행
  const startLabelPrecompute = useCallback((currentParams: CameraParams, currentSmoothHalf: number) => {
    const id = ++precomputeIdRef.current;
    const newMap = new Map<number, LabelCache>();
    const frames = allDroneFramesRef.current;
    const allSt = allGeoStationsRef.current;
    const allPoi = allPoisRef.current;
    const worldOrigin = worldOriginRef.current;
    const CLIP_Z = 0.1;
    const SMOOTH_HALF = currentSmoothHalf;

    if (!frames.length) return;

    const t0 = performance.now();
    let idx = 0;
    const CHUNK = 200;  // 한 번에 처리할 프레임 수

    const step = () => {
      if (precomputeIdRef.current !== id) return;  // 취소됨

      const end = Math.min(idx + CHUNK, frames.length);
      while (idx < end) {
        const drone = smoothFrame(frames, idx++, SMOOTH_HALF);

        const stationLabels: LabelCache['stationLabels'] = [];
        for (const st of allSt) {
          const snap = nearestCL(st.lat, st.lon);
          const cc = toCameraCoords(drone, snap?.lat ?? st.lat, snap?.lon ?? st.lon, snap?.z ?? st.z, currentParams, worldOrigin);
          if (cc.Zc < CLIP_Z) continue;
          const { pxRaw, pyRaw } = pixelFromCamera(cc, currentParams);
          if (pxRaw < -0.05 || pxRaw > 1.05 || pyRaw < -0.05 || pyRaw > 1.05) continue;
          stationLabels.push({ sx: pxRaw, sy: pyRaw, title: st.title });
        }

        const poiMarkers: LabelCache['poiMarkers'] = [];
        for (const poi of allPoi) {
          const poiZ = nearestCL(poi.lat, poi.lon)?.z ?? poi.z;
          const cc = toCameraCoords(drone, poi.lat, poi.lon, poiZ, currentParams, worldOrigin);
          if (cc.Zc < CLIP_Z) continue;
          const { pxRaw, pyRaw } = pixelFromCamera(cc, currentParams);
          if (pxRaw < -0.02 || pxRaw > 1.02 || pyRaw < -0.02 || pyRaw > 1.02) continue;
          poiMarkers.push({ x: pxRaw, y: pyRaw, title: poi.title, category: poi.category });
        }

        newMap.set(drone.frame, { stationLabels, poiMarkers });
      }

      if (idx < frames.length) {
        requestIdleCallback(step, { timeout: 200 });
      } else {
        labelMapRef.current = newMap;
        console.log(
          `[labelMap] complete ${(performance.now() - t0).toFixed(0)}ms | ${frames.length} frames × ${allSt.length + allPoi.length} items`
        );
      }
    };

    requestIdleCallback(step, { timeout: 200 });
  }, [nearestCL]);

  // 모든 데이터 로드 완료 시 사전 계산 시작
  useEffect(() => {
    if (!droneFramesLoaded || !geoDataLoaded || !clDataLoaded) return;
    startLabelPrecompute(paramsRef.current, smoothHalf);
  }, [droneFramesLoaded, geoDataLoaded, clDataLoaded, startLabelPrecompute, smoothHalf]);

  // params / smoothHalf 변경 시 사전 계산 재시작 (500ms debounce)
  useEffect(() => {
    if (!droneFramesLoaded || !geoDataLoaded || !clDataLoaded) return;
    const timer = setTimeout(() => startLabelPrecompute(params, smoothHalf), 500);
    return () => clearTimeout(timer);
  }, [params, smoothHalf, droneFramesLoaded, geoDataLoaded, clDataLoaded, startLabelPrecompute]);

  // 현재 재생 시간 → 드론 프레임 ref 갱신
  useEffect(() => {
    if (!visible || !droneFramesLoaded) return;
    const frames = allDroneFramesRef.current;
    if (!frames.length) return;
    let best = frames[0], bestIdx = 0, bestD = Math.abs((best.frame ?? 0) / VIDEO_FPS - currentTime);
    for (let i = 0; i < frames.length; i++) {
      const d = Math.abs(frames[i].frame / VIDEO_FPS - currentTime);
      if (d < bestD) { bestD = d; best = frames[i]; bestIdx = i; }
      if (bestD < 1 / VIDEO_FPS / 2) break;
    }
    currentFrameNumRef.current = best.frame;
    currentFrameIdxRef.current = bestIdx;
    currentTimeSecRef.current = currentTime;
    timeUpdateWallRef.current = performance.now();
    if (currentDroneFrameRef.current?.frame !== best.frame) {
      currentDroneFrameRef.current = best;
      setPanelDroneFrame(best);
    }
  }, [currentTime, visible, droneFramesLoaded]);

  // 중심선 + 나침반 캐시 빌드 (per-frame, 텍스트 계산 없음)
  useEffect(() => {
    if (!currentDroneFrameRef.current || !visible) { renderCacheRef.current = null; return; }

    const t0 = performance.now();
    // 중심선도 smoothFrame 적용 (텍스트와 동일한 스무딩)
    const frames = allDroneFramesRef.current;
    const drone = frames.length
      ? smoothFrame(frames, currentFrameIdxRef.current, smoothHalfRef.current)
      : currentDroneFrameRef.current;
    const params = paramsRef.current;
    const worldOrigin = worldOriginRef.current;
    const allCL = allCenterlinePointsRef.current;
    const CLIP_Z = 0.1;
    const SCREEN_M = 200;

    // 선로 중심선
    const camPts: CameraCoords[] = allCL.map(pt =>
      toCameraCoords(drone, pt.lat, pt.lon, pt.z, params, worldOrigin)
    );
    const centerlineSegs: [number, number, number, number][] = [];
    for (let i = 0; i < camPts.length - 1; i++) {
      const c1 = camPts[i], c2 = camPts[i + 1];
      const z1 = c1.Zc, z2 = c2.Zc;
      if (z1 < CLIP_Z && z2 < CLIP_Z) continue;
      let px1: number, py1: number, px2: number, py2: number;
      if (z1 >= CLIP_Z && z2 >= CLIP_Z) {
        const p1 = pixelFromCamera(c1, params), p2 = pixelFromCamera(c2, params);
        px1 = p1.pxRaw; py1 = p1.pyRaw; px2 = p2.pxRaw; py2 = p2.pyRaw;
      } else {
        const t = (CLIP_Z - z1) / (z2 - z1);
        const cClip: CameraCoords = { Xc: c1.Xc + t*(c2.Xc-c1.Xc), Yc: c1.Yc + t*(c2.Yc-c1.Yc), Zc: CLIP_Z };
        if (z1 < CLIP_Z) {
          const p1 = pixelFromCamera(cClip, params), p2 = pixelFromCamera(c2, params);
          px1 = p1.pxRaw; py1 = p1.pyRaw; px2 = p2.pxRaw; py2 = p2.pyRaw;
        } else {
          const p1 = pixelFromCamera(c1, params), p2 = pixelFromCamera(cClip, params);
          px1 = p1.pxRaw; py1 = p1.pyRaw; px2 = p2.pxRaw; py2 = p2.pyRaw;
        }
      }
      const oc = (px: number, py: number) =>
        (px < -0.1 ? 1 : 0) | (px > 1.1 ? 2 : 0) |
        (py < -0.1 ? 4 : 0) | (py > 1.1 ? 8 : 0);
      if (oc(px1, py1) & oc(px2, py2)) continue;
      centerlineSegs.push([px1, py1, px2, py2]);
    }

    renderCacheRef.current = {
      centerlineSegs,
      effectiveYaw: drone.yaw + params.yawOffset,
      hFovRad: 2 * Math.atan((params.sensorW ?? 36) / (2 * params.focalLen)),
      clCount: allCL.length,
      poiCount: allPoisRef.current.length,
    };

    const elapsed = performance.now() - t0;
    console.log(
      `[cache] ${elapsed.toFixed(1)}ms | CL segs=${centerlineSegs.length}/${allCL.length} | frame=${drone.frame}`
    );
  }, [panelDroneFrame, params, visible]);

  // ResizeObserver
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    const parent = canvas.parentElement;
    if (!parent) return;
    const ro = new ResizeObserver(entries => {
      for (const e of entries) {
        canvas.width = Math.round(e.contentRect.width);
        canvas.height = Math.round(e.contentRect.height);
        canvasSizeRef.current = { w: canvas.width, h: canvas.height };
      }
    });
    ro.observe(parent);
    canvas.width = parent.clientWidth;
    canvas.height = parent.clientHeight;
    canvasSizeRef.current = { w: canvas.width, h: canvas.height };
    return () => ro.disconnect();
  }, []);

  // RAF 렌더 루프 — 계산 없이 캐시/Map 조회만
  useEffect(() => {
    const canvas = canvasRef.current;
    if (!canvas) return;
    let rafId = 0;

    const draw = () => {
      rafId = requestAnimationFrame(draw);
      const ctx = canvas.getContext('2d');
      if (!ctx) return;
      const { w: W, h: H } = canvasSizeRef.current;
      ctx.clearRect(0, 0, W, H);
      if (!visibleRef.current) return;

      const cache = renderCacheRef.current;
      if (!cache) return;

      // 선로 중심선
      if (cache.centerlineSegs.length > 0) {
        ctx.strokeStyle = 'rgba(255,50,50,0.85)';
        ctx.lineWidth = 3;
        ctx.setLineDash([]);
        ctx.beginPath();
        for (const [px1, py1, px2, py2] of cache.centerlineSegs) {
          ctx.moveTo(px1*W, py1*H);
          ctx.lineTo(px2*W, py2*H);
        }
        ctx.stroke();
      }

      // 텍스트 — Map 조회 + 프레임 간 보간 (30fps 데이터 → 60fps 부드러운 표시)
      const frameNum = currentFrameNumRef.current;
      // performance.now()로 마지막 prop 업데이트 이후 경과 시간을 더해 현재 재생 위치 추정
      const elapsed = (performance.now() - timeUpdateWallRef.current) / 1000;
      const estTime = currentTimeSecRef.current + elapsed;
      const frac = Math.min(0.999, (estTime * VIDEO_FPS) - frameNum);  // 0~0.999
      const labelsA = labelMapRef.current.get(frameNum);
      const labelsB = labelMapRef.current.get(frameNum + 1);

      // 두 프레임 사이 픽셀 좌표 보간
      const interpY = (a: number, b: number | undefined) => b !== undefined ? a + (b - a) * frac : a;

      if (labelsA) {
        const α = emaAlphaRef.current;

        // 측점 라벨 — 보간 후 EMA 적용
        ctx.font = 'bold 18px monospace';
        ctx.textAlign = 'left';
        ctx.textBaseline = 'middle';
        labelsA.stationLabels.forEach((stA, i) => {
          const stB = labelsB?.stationLabels[i];
          const targetX = interpY(stA.sx, stB?.sx);
          const targetY = interpY(stA.sy, stB?.sy);
          const prev = displayedStRef.current.get(stA.title);
          const dispX = prev ? prev.x + (targetX - prev.x) * α : targetX;
          const dispY = prev ? prev.y + (targetY - prev.y) * α : targetY;
          displayedStRef.current.set(stA.title, { x: dispX, y: dispY });
          const x = dispX*W, y = dispY*H;
          // 마커 선
          ctx.strokeStyle = 'rgba(255,100,100,0.95)'; ctx.lineWidth = 2.5;
          ctx.beginPath(); ctx.moveTo(x, y-10); ctx.lineTo(x, y+10); ctx.stroke();
          // 텍스트 테두리
          const lx = Math.max(2, x + 8);
          ctx.strokeStyle = 'rgba(0,0,0,0.85)'; ctx.lineWidth = 4;
          ctx.lineJoin = 'round';
          ctx.strokeText(cleanTitle(stA.title), lx, y);
          // 텍스트 본문
          ctx.fillStyle = 'rgba(255,200,200,1.0)';
          ctx.fillText(cleanTitle(stA.title), lx, y);
        });

        // POI 마커 — 보간 후 EMA 적용
        ctx.font = 'bold 20px sans-serif';
        labelsA.poiMarkers.forEach((poiA, i) => {
          const poiB = labelsB?.poiMarkers[i];
          const targetX = interpY(poiA.x, poiB?.x);
          const targetY = interpY(poiA.y, poiB?.y);
          const prev = displayedPoiRef.current.get(poiA.title);
          const dispX = prev ? prev.x + (targetX - prev.x) * α : targetX;
          const dispY = prev ? prev.y + (targetY - prev.y) * α : targetY;
          displayedPoiRef.current.set(poiA.title, { x: dispX, y: dispY });
          const px = dispX*W, py = dispY*H, r = 10;
          // 십자 마커
          ctx.strokeStyle = '#64c8ff'; ctx.lineWidth = 2.5;
          ctx.beginPath();
          ctx.moveTo(px-r, py); ctx.lineTo(px+r, py);
          ctx.moveTo(px, py-r); ctx.lineTo(px, py+r);
          ctx.stroke();
          // 이모지 + 텍스트
          const emoji = CATEGORY_EMOJI[poiA.category] ?? '📌';
          const label = `${emoji} ${cleanTitle(poiA.title)}`;
          const lx = Math.max(2, px + 14);
          ctx.strokeStyle = 'rgba(0,0,0,0.85)'; ctx.lineWidth = 4;
          ctx.lineJoin = 'round';
          ctx.textBaseline = 'middle';
          ctx.strokeText(label, lx, py);
          ctx.fillStyle = '#64c8ff';
          ctx.fillText(label, lx, py);
          ctx.textBaseline = 'alphabetic';
        });
      }

      // 나침반 HUD
      {
        const cx = W-54, cy = H-54-H*0.05, r = 38;
        ctx.beginPath(); ctx.arc(cx, cy, r, 0, Math.PI*2);
        ctx.fillStyle = 'rgba(0,0,0,0.55)'; ctx.fill();
        ctx.strokeStyle = 'rgba(255,255,255,0.25)'; ctx.lineWidth = 1; ctx.stroke();
        ctx.font = 'bold 9px sans-serif'; ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
        for (const [label, deg] of [['N',0],['E',90],['S',180],['W',270]] as const) {
          const rad = (deg-90)*Math.PI/180;
          ctx.fillStyle = label==='N' ? '#ff6060' : 'rgba(255,255,255,0.5)';
          ctx.fillText(label, cx+Math.cos(rad)*(r-9), cy+Math.sin(rad)*(r-9));
        }
        const yawRad = (cache.effectiveYaw-90)*Math.PI/180;
        const tx = cx+Math.cos(yawRad)*(r-14), ty = cy+Math.sin(yawRad)*(r-14);
        ctx.beginPath(); ctx.moveTo(cx, cy); ctx.lineTo(tx, ty);
        ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 2.5; ctx.stroke();
        const ha = 0.42, hl = 9;
        ctx.beginPath();
        ctx.moveTo(tx, ty); ctx.lineTo(tx-hl*Math.cos(yawRad-ha), ty-hl*Math.sin(yawRad-ha));
        ctx.moveTo(tx, ty); ctx.lineTo(tx-hl*Math.cos(yawRad+ha), ty-hl*Math.sin(yawRad+ha));
        ctx.strokeStyle = '#ffd700'; ctx.lineWidth = 2; ctx.stroke();
        ctx.beginPath(); ctx.moveTo(cx, cy);
        ctx.arc(cx, cy, r-2, yawRad-cache.hFovRad/2, yawRad+cache.hFovRad/2); ctx.closePath();
        ctx.fillStyle = 'rgba(255,215,0,0.12)'; ctx.fill();
        ctx.strokeStyle = 'rgba(255,215,0,0.35)'; ctx.lineWidth = 1; ctx.stroke();
        ctx.font = '9px monospace'; ctx.textBaseline = 'top'; ctx.fillStyle = '#ffd700';
        ctx.fillText(`${((cache.effectiveYaw+360)%360).toFixed(1)}°`, cx, cy+r+2);
        ctx.textAlign = 'left'; ctx.textBaseline = 'alphabetic';
      }

      // 범례
      if (cache.clCount > 0 || cache.poiCount > 0) {
        const lines = [
          ...(cache.clCount > 0 ? [{ color: 'rgba(255,60,60,0.9)', text: `— 선로중심선 (${cache.clCount}점)` }] : []),
          ...(cache.poiCount > 0 ? [{ color: '#64c8ff', text: `+ 지장물 ${cache.poiCount}개` }] : []),
        ];
        ctx.fillStyle = 'rgba(0,0,0,0.6)';
        ctx.fillRect(6, 6, 160, lines.length*15+6);
        lines.forEach(({ color, text }, i) => {
          ctx.font = '10px sans-serif'; ctx.fillStyle = color;
          ctx.fillText(text, 12, 20+i*15);
        });
      }
    };

    rafId = requestAnimationFrame(draw);
    return () => cancelAnimationFrame(rafId);
  }, []);

  return (
    <>
      <canvas
        ref={canvasRef}
        className="absolute inset-0 pointer-events-none z-20"
        style={{ width: '100%', height: '100%', display: visible ? undefined : 'none' }}
      />
      <div className="absolute top-2 right-2 z-30 flex flex-col items-end gap-1">
        <button onClick={() => setShowControls(v => !v)}
          className="text-xs bg-black/70 hover:bg-black/90 text-gray-200 px-2 py-1 rounded border border-gray-500 shadow">
          {showControls ? '▲ 카메라 파라미터' : '▼ 카메라 파라미터'}
        </button>
        {showControls && (
          <div className="bg-black/90 border border-gray-600 rounded p-3 text-white w-72 select-none max-h-[80vh] overflow-y-auto shadow-xl">
            <div className="text-[10px] text-gray-500 uppercase tracking-wider mb-1.5 border-b border-gray-700 pb-2 mb-2">스무딩 <span className="text-gray-600">(재계산 500ms 후)</span></div>
            <div className="mb-3 space-y-2">
              <ParamRow label="smooth" value={smoothHalf} min={0} max={60} step={1} unit="fr" decimals={0} onChange={v => setSmoothHalf(Math.round(v))} />
              <div className="text-[10px] text-gray-600 text-right">±{smoothHalf}fr = ±{(smoothHalf * 1000 / (30000/1001)).toFixed(0)}ms</div>
              <ParamRow label="EMA α" value={emaAlpha} min={0.01} max={1.0} step={0.01} unit="" decimals={2} onChange={v => setEmaAlpha(v)} />
              <div className="text-[10px] text-gray-600 text-right">α={emaAlpha.toFixed(2)} → lag≈{(1000/60*(1/emaAlpha - 1)).toFixed(0)}ms</div>
            </div>
            <div className="text-[10px] text-gray-500 uppercase tracking-wider mb-1.5">자세 보정 오프셋<span className="ml-1 text-gray-600">(SRT + offset)</span></div>
            <div className="space-y-2 mb-3">
              <ParamRow label="Yaw ±" value={params.yawOffset} min={-180} max={180} step={0.1} unit="°" decimals={1} onChange={v => setParam('yawOffset', v)} />
              <ParamRow label="Pitch ±" value={params.pitch} min={-45} max={45} step={0.1} unit="°" decimals={1} onChange={v => setParam('pitch', v)} />
              <ParamRow label="Roll ±" value={params.roll} min={-45} max={45} step={0.1} unit="°" decimals={1} onChange={v => setParam('roll', v)} />
            </div>
            <div className="text-[10px] text-gray-500 uppercase tracking-wider mb-1.5 border-t border-gray-700 pt-2">위치 보정 (드론 GPS 오프셋)</div>
            <div className="space-y-2 mb-3">
              <ParamRow label="off X" value={params.offX} min={-500} max={500} step={0.1} unit="m" decimals={1} onChange={v => setParam('offX', v)} />
              <ParamRow label="off Y" value={params.offY} min={-500} max={500} step={0.1} unit="m" decimals={1} onChange={v => setParam('offY', v)} />
              <ParamRow label="off Z" value={params.offZ} min={-200} max={200} step={0.1} unit="m" decimals={1} onChange={v => setParam('offZ', v)} />
            </div>
            <div className="text-[10px] text-gray-500 uppercase tracking-wider mb-1.5 border-t border-gray-700 pt-2">내부표정 (초점·주점·센서)</div>
            <div className="space-y-2 mb-3">
              <ParamRow label="f" value={params.focalLen} min={10} max={100} step={0.1} unit="mm" decimals={1} onChange={v => setParam('focalLen', v)} />
              <ParamRow label="cx₀" value={params.cx0} min={-0.5} max={0.5} step={0.005} unit="" decimals={3} onChange={v => setParam('cx0', v)} />
              <ParamRow label="cy₀" value={params.cy0} min={-0.5} max={0.5} step={0.005} unit="" decimals={3} onChange={v => setParam('cy0', v)} />
              <ParamRow label="sen W" value={params.sensorW} min={10} max={50} step={0.05} unit="mm" decimals={2} onChange={v => setParam('sensorW', v)} />
              <ParamRow label="sen H" value={params.sensorH} min={6} max={36} step={0.05} unit="mm" decimals={2} onChange={v => setParam('sensorH', v)} />
            </div>
            {panelDroneFrame && (
              <div className="border-t border-gray-700 pt-2 mb-2 text-[10px] text-gray-400 font-mono space-y-0.5">
                <div>yaw: {((panelDroneFrame.yaw+params.yawOffset+360)%360).toFixed(1)}° pitch: {(panelDroneFrame.pitch+params.pitch).toFixed(1)}° roll: {(panelDroneFrame.roll+params.roll).toFixed(1)}°</div>
                <div>f: {params.focalLen.toFixed(1)}mm hFOV: {(2*Math.atan((params.sensorW??36)/(2*params.focalLen))*180/Math.PI).toFixed(1)}°</div>
                <div>offX: {params.offX.toFixed(1)}m offY: {params.offY.toFixed(1)}m offZ: {params.offZ.toFixed(1)}m</div>
              </div>
            )}
            <div className="border-t border-gray-700 pt-2 flex items-center justify-end">
              <button onClick={() => setParams({ ...DEFAULT_CAMERA_PARAMS })}
                className="text-[11px] text-gray-400 hover:text-white border border-gray-600 hover:border-gray-400 px-2 py-0.5 rounded transition-colors">
                초기화
              </button>
            </div>
          </div>
        )}
      </div>
    </>
  );
}
