/**
 * 선박 배치 렌더러
 * 참조: mda-react-front/src/common/deck.ts
 * 참조: mda-react-front/src/tracking/utils/ReplayBatchRenderer.ts
 *
 * 역할:
 * - 필터 캐시 생성 및 O(1) 필터링
 * - 뷰포트 범위 필터링
 * - 통합선박 처리
 * - 밀도 제한 클러스터링 (우선순위 기반)
 * - 적응형 렌더링 간격 조정
 * - requestAnimationFrame 기반 렌더링
 */

import useShipStore, {
  buildDynamicPrioritySet,
  isAnyEquipmentActive,
  mapNationalCode as storeMapNationalCode,
  initialKindCounts,
  INSHORE_TIMEOUT_MS,
  OFFSHORE_TIMEOUT_MS,
  RADAR_TIMEOUT_MS,
  SIGNAL_SOURCE_RADAR,
} from '../stores/shipStore';
import useTrackingModeStore, { isWithinRadius, NM_TO_METERS } from '../stores/trackingModeStore';
import {
  SIGNAL_KIND_CODE_FISHING,
  SIGNAL_KIND_CODE_KCGV,
  SIGNAL_KIND_CODE_PASSENGER,
  SIGNAL_KIND_CODE_CARGO,
  SIGNAL_KIND_CODE_TANKER,
  SIGNAL_KIND_CODE_GOV,
  SIGNAL_KIND_CODE_NORMAL,
  SIGNAL_KIND_CODE_BUOY,
  SOURCE_PRIORITY_RANK,
  SOURCE_TO_ACTIVE_KEY,
} from '../types/constants';

// =====================
// 렌더링 설정
// 위성통신망 환경 최적화: 최소 트래픽, 최소 스펙
// =====================
const RENDER_CONFIG = {
  defaultMinInterval: 1000, // 기본 최소 렌더링 간격 (1초)
  maxInterval: 5000,        // 최대 렌더링 간격 (5초)
  targetRenderTime: 100,    // 목표 렌더링 시간 (10fps 기준)
  maxRenderTime: 500,       // 최대 허용 렌더링 시간
};

// =====================
// 줌 레벨별 최소 렌더링 간격
// 낮은 줌 = 많은 선박 = 긴 간격
// =====================
const ZOOM_MIN_INTERVAL = {
  // zoom < 8: 광역 (전국/동아시아)
  7: 4000,
  // zoom 8-9: 중광역 (해역)
  8: 3500,
  9: 3000,
  // zoom 10-11: 중간 (항만 주변)
  10: 2500,
  11: 2000,
  // zoom 12-13: 상세 (항만)
  12: 1500,
  13: 1500,
  // zoom >= 14: 확대 (선석)
  14: 1000,
};

/**
 * 줌 레벨에 따른 최소 렌더링 간격 반환
 * @param {number} zoom - 현재 줌 레벨
 * @returns {number} 최소 렌더링 간격 (ms)
 */
function getMinIntervalByZoom(zoom) {
  const zoomInt = Math.floor(zoom);

  if (zoomInt <= 7) return ZOOM_MIN_INTERVAL[7];
  if (zoomInt >= 14) return ZOOM_MIN_INTERVAL[14];

  return ZOOM_MIN_INTERVAL[zoomInt] || RENDER_CONFIG.defaultMinInterval;
}

// =====================
// 밀도 제한 설정 (선박 아이콘 클러스터링)
// 참조: mda-react-front/src/util/realTimeLayerUtil.ts
//
// 줌 레벨별 셀당 최대 선박 수 및 그리드 크기 설정
// - 낮은 줌: 더 적은 개수 (밀집 지역 성능 최적화)
// - 높은 줌: 더 많은 개수 (상세 보기)
// =====================
const DENSITY_LIMITS = [
  { maxZoom: 5, maxPerCell: 20, gridSizeMultiplier: 120 },
  { maxZoom: 6, maxPerCell: 25, gridSizeMultiplier: 100 },
  { maxZoom: 7, maxPerCell: 33, gridSizeMultiplier: 80 },
  { maxZoom: 8, maxPerCell: 35, gridSizeMultiplier: 75 },
  { maxZoom: 9, maxPerCell: 38, gridSizeMultiplier: 70 },
  { maxZoom: 10, maxPerCell: 40, gridSizeMultiplier: 55 },
  { maxZoom: 11, maxPerCell: 43, gridSizeMultiplier: 40 },
  { maxZoom: Infinity, maxPerCell: Infinity, gridSizeMultiplier: 30 },
];

/**
 * 밀도 제한 시 선박 우선순위 (낮을수록 높은 우선순위)
 * 우선순위: 관심선박 > 함정 > 관공선 > 여객선 > 위험물 > 유조선 > 화물선 > 어선 > 기타 > 어망/부이
 */
const SHIP_KIND_PRIORITY = {
  [SIGNAL_KIND_CODE_KCGV]: 2,      // 함정
  [SIGNAL_KIND_CODE_GOV]: 3,       // 관공선
  [SIGNAL_KIND_CODE_PASSENGER]: 4, // 여객선
  [SIGNAL_KIND_CODE_TANKER]: 6,    // 유조선
  [SIGNAL_KIND_CODE_CARGO]: 7,     // 화물선
  [SIGNAL_KIND_CODE_FISHING]: 8,   // 어선
  [SIGNAL_KIND_CODE_NORMAL]: 9,    // 기타
  [SIGNAL_KIND_CODE_BUOY]: 10,     // 어망/부이
};
const PRIORITY_FAVORITE = 0;    // 관심선박 (최우선)
const PRIORITY_HAZARDOUS = 5;   // 위험물 (여객선과 유조선 사이)
const PRIORITY_DEFAULT = 11;    // 기본값 (최하위)

/**
 * 선박 우선순위 점수 계산
 * @param {Object} ship - 선박 데이터
 * @param {Set} favoriteSet - 관심선박 ID Set
 * @returns {number} 우선순위 점수 (낮을수록 높은 우선순위)
 */
function getShipPriority(ship, favoriteSet) {
  // 관심선박 체크 (최우선)
  const favoriteKey = `${ship.targetId}_${ship.signalSourceCode}`;
  if (favoriteSet && favoriteSet.has(favoriteKey)) {
    return PRIORITY_FAVORITE;
  }

  // 위험물 체크 (hazardousCategory = '1')
  if (ship.hazardousCategory === '1') {
    return PRIORITY_HAZARDOUS;
  }

  // 선종 코드 기반 우선순위
  return SHIP_KIND_PRIORITY[ship.signalKindCode] ?? PRIORITY_DEFAULT;
}

/**
 * 줌레벨에 따른 밀도 설정 반환
 * @param {number} zoomLevel - 현재 줌 레벨
 * @returns {Object} 밀도 설정 { maxZoom, maxPerCell, gridSizeMultiplier }
 */
function getDensityConfig(zoomLevel) {
  for (const config of DENSITY_LIMITS) {
    if (zoomLevel <= config.maxZoom) {
      return config;
    }
  }
  return DENSITY_LIMITS[DENSITY_LIMITS.length - 1];
}

/**
 * 선박 아이콘용 밀도 제한 필터링 (우선순위 기반)
 * 참조: mda-react-front/src/util/realTimeLayerUtil.ts - applyDensityLimit()
 *
 * - 그리드 기반으로 동일 영역 내 최대 N척까지만 렌더링
 * - 밀집 지역도 N척이 겹쳐 보여서 "빈 공간"처럼 보이지 않음
 * - 줌레벨이 높아지면 제한이 완화되어 더 많은 선박 표시
 * - 우선순위: 관심선박 > 함정 > 관공선 > 여객선 > 위험물 > 유조선 > 화물선 > 어선 > 기타 > 어망/부이
 *
 * @param {Array} ships - 필터링된 선박 데이터
 * @param {number} zoomLevel - 현재 줌레벨
 * @param {Set} favoriteSet - 관심선박 ID Set (optional)
 * @returns {Array} 밀도 제한이 적용된 선박 데이터
 */
function applyDensityLimit(ships, zoomLevel, favoriteSet = null) {
  const config = getDensityConfig(zoomLevel);

  // 제한 없음 설정이면 원본 반환
  if (config.maxPerCell === Infinity) {
    return ships;
  }

  // 우선순위 기반 정렬 (낮은 점수 = 높은 우선순위)
  const sortedShips = [...ships].sort((a, b) => {
    return getShipPriority(a, favoriteSet) - getShipPriority(b, favoriteSet);
  });

  // 그리드 크기 계산 (줌레벨에 따라 조정)
  const gridSize = Math.pow(2, -zoomLevel) * config.gridSizeMultiplier;

  // 그리드별 선박 수 카운트
  const gridCounts = new Map();
  const result = [];

  const len = sortedShips.length;
  for (let i = 0; i < len; i++) {
    const ship = sortedShips[i];
    const gridX = Math.floor(ship.longitude / gridSize);
    const gridY = Math.floor(ship.latitude / gridSize);
    const gridKey = `${gridX},${gridY}`;

    const currentCount = gridCounts.get(gridKey) || 0;

    // 셀 내 최대 개수 미만이면 포함
    if (currentCount < config.maxPerCell) {
      result.push(ship);
      gridCounts.set(gridKey, currentCount + 1);
    }
    // 초과하면 스킵 (우선순위 낮은 선박이 제외됨)
  }

  // deck.gl은 배열 뒤쪽 아이템이 위에 그려짐
  // 우선순위 높은 선박이 위에 보이도록 역순 반환
  return result.reverse();
}

// =====================
// 필터 캐시 인터페이스
// 참조: mda-react-front/src/common/deck.ts (52-108)
// =====================

/**
 * 필터 캐시 생성
 * 렌더링 시작 시 1회 호출하여 Set 기반 O(1) lookup 가능하게 함
 *
 * @returns {Object} 필터 캐시 객체
 */
function buildFilterCache() {
  const { kindVisibility, sourceVisibility, nationalVisibility, isShipVisible, isIntegrate, darkSignalVisible, darkSignalIds, features } = useShipStore.getState();

  // 활성화된 선종 코드 Set
  const enabledKinds = new Set();
  Object.entries(kindVisibility).forEach(([code, isChecked]) => {
    if (isChecked) {
      enabledKinds.add(code);
    }
  });

  // 활성화된 신호원 코드 Set
  const enabledSources = new Set();
  Object.entries(sourceVisibility).forEach(([code, isChecked]) => {
    if (isChecked) {
      enabledSources.add(code);
    }
  });

  // 활성화된 국적 코드 Set
  const enabledNationals = new Set();
  Object.entries(nationalVisibility).forEach(([code, isChecked]) => {
    if (isChecked) {
      enabledNationals.add(code);
    }
  });

  // 동적 우선순위 Set (통합 모드에서만 생성)
  // 참조: mda-react-front/docs/dynamic-priority.md §5
  let dynamicPrioritySet = null;
  if (isIntegrate) {
    dynamicPrioritySet = buildDynamicPrioritySet(features, enabledSources, darkSignalIds);
  }

  return {
    enabledKinds,
    enabledSources,
    enabledNationals,
    isShipVisible,
    isIntegrate,
    darkSignalVisible,
    darkSignalIds,
    dynamicPrioritySet,
  };
}

/**
 * 국적 코드 매핑 (실제 국적 코드 -> 필터 코드)
 * @param {string} nationalCode - 선박의 국적 코드
 * @returns {string} 필터용 국적 코드
 */
function mapNationalCode(nationalCode) {
  if (!nationalCode) return 'OTHER';
  const code = nationalCode.toUpperCase();
  if (code === 'KR' || code === 'KOR' || code === '440') return 'KR';
  if (code === 'CN' || code === 'CHN' || code === '412' || code === '413' || code === '414') return 'CN';
  if (code === 'JP' || code === 'JPN' || code === '431' || code === '432') return 'JP';
  if (code === 'KP' || code === 'PRK' || code === '445') return 'KP';
  return 'OTHER';
}

/**
 * 캐시된 필터를 사용한 필터링 (O(1) lookup)
 * 참조: mda-react-front/src/common/deck.ts - applyFilterWithCache()
 *
 * @param {Object} ship - 선박 데이터
 * @param {Object} cache - 필터 캐시
 * @returns {boolean} 필터 통과 여부
 */
function applyFilterWithCache(ship, cache) {
  // 전체 선박 표시 Off
  if (!cache.isShipVisible) return false;

  // ① 다크시그널: 독립 필터 (선종/신호원/국적 무시, darkSignalVisible만 참조)
  // 통합모드 체크보다 먼저 실행해야 통합 다크시그널 선박도 렌더링됨
  if (cache.darkSignalIds.has(ship.featureId)) return cache.darkSignalVisible;

  // ② 통합 모드 필터: 동적 우선순위 기반 대표 선박만 표시
  // 참조: mda-react-front/docs/dynamic-priority.md §5
  if (cache.isIntegrate && cache.dynamicPrioritySet) {
    const targetId = ship.targetId;
    if (targetId && targetId.includes('_')) {
      // 통합선박: dynamicPrioritySet에 있는 대표만 통과
      if (!cache.dynamicPrioritySet.has(ship.featureId)) return false;
    }
    // 단독선박: 동적 우선순위 체크 스킵
  }

  // ③ 선종 필터 (Set.has = O(1))
  if (!cache.enabledKinds.has(ship.signalKindCode)) return false;

  // 신호원 필터 (Set.has = O(1))
  if (!cache.enabledSources.has(ship.signalSourceCode)) return false;

  // 국적 필터 (Set.has = O(1))
  const mappedNational = mapNationalCode(ship.nationalCode);
  if (!cache.enabledNationals.has(mappedNational)) return false;

  return true;
}

/**
 * 뷰포트 범위 내 선박 필터링
 * 참조: mda-react-front/src/util/realTimeLayerUtil.ts - filterFeaturesWithBounds()
 *
 * @param {Array} ships - 선박 배열
 * @param {Object} bounds - 뷰포트 범위 { minLon, maxLon, minLat, maxLat }
 * @returns {Array} 범위 내 선박 배열
 */
function filterByViewport(ships, bounds) {
  if (!bounds) return ships;

  const { minLon, maxLon, minLat, maxLat } = bounds;

  return ships.filter((ship) => {
    const lon = ship.longitude;
    const lat = ship.latitude;
    return lon >= minLon && lon <= maxLon && lat >= minLat && lat <= maxLat;
  });
}

// =====================
// 반경 필터링 (추적 모드)
// =====================
const LON_DEGREE_METERS = 91000; // 중위도(35도) 기준 경도 1도당 약 91km
const LAT_DEGREE_METERS = 111000; // 위도 1도당 약 111km

/**
 * 반경 필터 상태 가져오기
 * @returns {Object} { isActive, center, radiusNM, boundingBox }
 */
function getRadiusFilterState() {
  const state = useTrackingModeStore.getState();
  const { mode, trackedShip, radiusNM } = state;

  const isActive = mode === 'ship' && trackedShip !== null;

  if (!isActive || !trackedShip || !trackedShip.longitude || !trackedShip.latitude) {
    return { isActive: false, center: null, radiusNM: 0, boundingBox: null };
  }

  const center = { lon: trackedShip.longitude, lat: trackedShip.latitude };

  // Bounding Box 계산 (빠른 사전 필터용)
  const radiusMeters = radiusNM * NM_TO_METERS;
  const lonDelta = radiusMeters / LON_DEGREE_METERS;
  const latDelta = radiusMeters / LAT_DEGREE_METERS;

  const boundingBox = {
    minLon: center.lon - lonDelta,
    maxLon: center.lon + lonDelta,
    minLat: center.lat - latDelta,
    maxLat: center.lat + latDelta,
  };

  return { isActive: true, center, radiusNM, boundingBox };
}

/**
 * 반경 필터링 적용
 * - 1단계: Bounding Box 체크 (빠른 사각형 체크)
 * - 2단계: Haversine 거리 계산 (정확한 원형 체크)
 *
 * @param {Array} ships - 선박 배열
 * @param {Object} radiusState - 반경 필터 상태
 * @returns {Array} 반경 내 선박 배열
 */
function filterByRadius(ships, radiusState) {
  const { isActive, center, radiusNM, boundingBox } = radiusState;

  if (!isActive || !center || !boundingBox) {
    return ships;
  }

  return ships.filter((ship) => {
    if (!ship.longitude || !ship.latitude) return false;

    // 1단계: Bounding Box 체크
    if (
      ship.longitude < boundingBox.minLon ||
      ship.longitude > boundingBox.maxLon ||
      ship.latitude < boundingBox.minLat ||
      ship.latitude > boundingBox.maxLat
    ) {
      return false;
    }

    // 2단계: 정확한 원형 거리 체크
    return isWithinRadius(ship, center.lon, center.lat, radiusNM);
  });
}

// =====================
// 카운트 + 타임아웃 cleanup 통합 함수
// 참조: mda-react-front/src/common/deck.ts (271-471)
// =====================
const LIVE_COUNT_THROTTLE_MS = 5000;

let liveCountCache = {
  lastCalcTime: 0,
  lastFilterHash: '',
};

/**
 * 필터 해시 생성 (필터 변경 감지용)
 * 참조: mda-react-front/src/common/deck.ts (488-511)
 */
function generateFilterHash() {
  const { kindVisibility, sourceVisibility, nationalVisibility,
          isIntegrate, darkSignalVisible } = useShipStore.getState();
  const kinds = Object.entries(kindVisibility).filter(([,v]) => v).map(([k]) => k).join(',');
  const sources = Object.entries(sourceVisibility).filter(([,v]) => v).map(([k]) => k).join(',');
  const nationals = Object.entries(nationalVisibility).filter(([,v]) => v).map(([k]) => k).join(',');
  return `${kinds}|${sources}|${nationals}|${isIntegrate?1:0}|${darkSignalVisible?1:0}`;
}

/**
 * 단일 패스로 타임아웃 cleanup + 카운트를 동시 수행
 * 참조: mda-react-front/src/common/deck.ts - calculateAndCleanupLiveShips
 *
 * @returns {{ kindCounts, darkSignalCount, totalCount, deleteIds, darkSignalConvertIds }}
 */
function calculateAndCleanupLiveShips() {
  const state = useShipStore.getState();
  const { features, darkSignalIds, isIntegrate,
          kindVisibility, sourceVisibility, nationalVisibility } = state;

  // 반경 필터 상태
  const radiusState = getRadiusFilterState();

  const kindCounts = { ...initialKindCounts };
  let darkSignalCount = 0;
  const deleteIds = [];
  const darkSignalConvertIds = [];
  const now = Date.now();

  // enabledSources Set
  const enabledSources = new Set();
  Object.entries(sourceVisibility).forEach(([code, on]) => { if (on) enabledSources.add(code); });

  const seenTargetIds = new Set();
  const bestByTargetId = new Map(); // 통합모드용

  features.forEach((ship, featureId) => {
    // ① 이미 다크시그널 → 카운트만, 즉시 리턴
    if (darkSignalIds.has(featureId)) {
      // 반경 필터가 활성화된 경우, 반경 내에 있을 때만 카운트
      if (radiusState.isActive) {
        if (!ship.longitude || !ship.latitude) return;
        const inBounds = ship.longitude >= radiusState.boundingBox.minLon &&
                         ship.longitude <= radiusState.boundingBox.maxLon &&
                         ship.latitude >= radiusState.boundingBox.minLat &&
                         ship.latitude <= radiusState.boundingBox.maxLat;
        if (!inBounds || !isWithinRadius(ship, radiusState.center.lon, radiusState.center.lat, radiusState.radiusNM)) {
          return;
        }
      }
      darkSignalCount++;
      return;
    }

    const sourceCode = ship.signalSourceCode;
    const elapsed = now - ship.receivedTimestamp;

    // ② 레이더: 만료 시 삭제, 유효 시 fall-through
    if (sourceCode === SIGNAL_SOURCE_RADAR) {
      if (elapsed > RADAR_TIMEOUT_MS) {
        deleteIds.push(featureId);
        return;
      }
      // 유효 레이더 → fall-through
    } else {
      // ③ LOST=0 + INSHORE 타임아웃 → 삭제
      if (!ship.lost && elapsed > INSHORE_TIMEOUT_MS) {
        deleteIds.push(featureId);
        return;
      }
      // ④ LOST=1 + OFFSHORE 타임아웃 → 다크시그널 전환
      if (ship.lost && elapsed > OFFSHORE_TIMEOUT_MS) {
        darkSignalConvertIds.push(featureId);
        darkSignalCount++;
        return;
      }
      // ⑤ 장비 전체 비활성 → 다크시그널 전환
      if (!isAnyEquipmentActive(ship)) {
        darkSignalConvertIds.push(featureId);
        darkSignalCount++;
        return;
      }
    }

    // ⑥ 반경 필터 체크 (카운트 전)
    if (radiusState.isActive) {
      if (!ship.longitude || !ship.latitude) return;
      const inBounds = ship.longitude >= radiusState.boundingBox.minLon &&
                       ship.longitude <= radiusState.boundingBox.maxLon &&
                       ship.latitude >= radiusState.boundingBox.minLat &&
                       ship.latitude <= radiusState.boundingBox.maxLat;
      if (!inBounds || !isWithinRadius(ship, radiusState.center.lon, radiusState.center.lat, radiusState.radiusNM)) {
        return;
      }
    }

    // ⑦ 카운트 대상
    const targetId = ship.targetId;
    const isIntegratedShip = targetId && (targetId.includes('_') || ship.integrate);

    if (isIntegrate && isIntegratedShip) {
      // 통합모드 + 통합선박 (언더스코어 또는 integrate 플래그) → 후보 수집
      const activeKey = SOURCE_TO_ACTIVE_KEY[sourceCode];
      if (!activeKey || ship[activeKey] !== '1') return;
      if (!enabledSources.has(sourceCode)) return;

      const rank = SOURCE_PRIORITY_RANK[sourceCode] ?? 99;
      const existing = bestByTargetId.get(targetId);
      if (!existing || rank < existing.rank) {
        bestByTargetId.set(targetId, { featureId, rank, ship });
      }
    } else {
      // 비통합 또는 단독선박
      // 단독 레이더(통합되지 않은 레이더)만 카운트 제외
      if (sourceCode === SIGNAL_SOURCE_RADAR && !isIntegratedShip) return;
      if (seenTargetIds.has(targetId)) return;

      if (!kindVisibility[ship.signalKindCode]) return;
      if (!sourceVisibility[ship.signalSourceCode]) return;
      const mapped = storeMapNationalCode(ship.nationalCode);
      if (!nationalVisibility[mapped]) return;

      if (targetId) seenTargetIds.add(targetId);
      if (ship.signalKindCode && kindCounts[ship.signalKindCode] !== undefined) {
        kindCounts[ship.signalKindCode]++;
      }
    }
  });

  // Phase B: 통합선박 대표 카운트
  if (isIntegrate) {
    bestByTargetId.forEach(({ ship }) => {
      if (!kindVisibility[ship.signalKindCode]) return;
      const mapped = storeMapNationalCode(ship.nationalCode);
      if (!nationalVisibility[mapped]) return;
      if (ship.signalKindCode && kindCounts[ship.signalKindCode] !== undefined) {
        kindCounts[ship.signalKindCode]++;
      }
    });
  }

  const totalCount = Object.values(kindCounts).reduce((a, b) => a + b, 0);
  return { kindCounts, darkSignalCount, totalCount, deleteIds, darkSignalConvertIds };
}

/**
 * 선박 배치 렌더러 클래스
 * 참조: mda-react-front/src/tracking/utils/ReplayBatchRenderer.ts
 */
class ShipBatchRenderer {
  constructor() {
    // 렌더링 상태
    this.renderState = {
      animationFrameId: null,
      pendingRender: false,
      isRendering: false,
      lastRenderTime: 0,
      currentInterval: RENDER_CONFIG.defaultMinInterval,
      currentZoom: 10, // 현재 줌 레벨
    };

    // 캐시
    this.cache = {
      filterCache: null,
      lastFilterHash: '',
      lastShipsData: [],        // 밀도 제한 적용된 선박 (아이콘 + 라벨 공통)
      lastFilteredCount: 0,     // 필터링된 선박 수 (밀도 제한 전)
      lastRenderTrigger: 0,
      favoriteSet: null,        // 관심선박 Set (향후 구현)
    };

    // 외부 콜백
    this.onRenderCallback = null;

    // 뷰포트 범위
    this.viewportBounds = null;
  }

  /**
   * 배치 렌더러 초기화
   * @param {Function} renderCallback - 레이어 렌더링 콜백
   *   (ships, trigger) => void
   *   - ships: 밀도 제한 적용된 선박 (아이콘 + 라벨 공통)
   *   - trigger: 렌더링 트리거 (주기적 갱신용)
   */
  initialize(renderCallback) {
    this.onRenderCallback = renderCallback;
    console.log('[ShipBatchRenderer] Initialized');
  }

  /**
   * 뷰포트 범위 업데이트
   * @param {Object} bounds - { minLon, maxLon, minLat, maxLat }
   */
  setViewportBounds(bounds) {
    this.viewportBounds = bounds;
  }

  /**
   * 줌 레벨 업데이트
   * 줌 변경 시 최소 렌더링 간격도 재조정
   * @param {number} zoom - 현재 줌 레벨
   * @returns {boolean} 정수 줌 레벨이 변경되었는지 여부
   */
  setZoom(zoom) {
    const prevZoom = this.renderState.currentZoom;
    const prevZoomInt = Math.floor(prevZoom);
    const newZoomInt = Math.floor(zoom);

    this.renderState.currentZoom = zoom;

    // 줌 레벨이 변경되면 최소 간격에 맞게 현재 간격 재조정
    const newMinInterval = getMinIntervalByZoom(zoom);
    const prevMinInterval = getMinIntervalByZoom(prevZoom);

    if (newMinInterval !== prevMinInterval) {
      // 새 최소 간격이 현재 간격보다 크면 현재 간격 증가
      if (newMinInterval > this.renderState.currentInterval) {
        this.renderState.currentInterval = newMinInterval;
      }
      // 새 최소 간격이 현재 간격보다 작고, 현재 간격이 이전 최소 간격과 같으면 감소
      else if (this.renderState.currentInterval === prevMinInterval) {
        this.renderState.currentInterval = newMinInterval;
      }
    }

    // 정수 줌 레벨 변경 여부 반환
    return prevZoomInt !== newZoomInt;
  }

  /**
   * 렌더링 요청
   * 다음 렌더링 사이클에 처리됨
   */
  requestRender() {
    if (this.renderState.pendingRender) return;

    this.renderState.pendingRender = true;

    // requestAnimationFrame 사용하여 렌더링 스케줄
    if (!this.renderState.animationFrameId) {
      this.scheduleRender();
    }
  }

  /**
   * 렌더링 스케줄링
   */
  scheduleRender() {
    const now = Date.now();
    const elapsed = now - this.renderState.lastRenderTime;
    const delay = Math.max(0, this.renderState.currentInterval - elapsed);

    this.renderState.animationFrameId = setTimeout(() => {
      this.renderState.animationFrameId = requestAnimationFrame(() => {
        this.executeRender();
      });
    }, delay);
  }

  /**
   * 실제 렌더링 실행
   */
  executeRender() {
    if (this.renderState.isRendering || !this.onRenderCallback) {
      this.renderState.animationFrameId = null;
      return;
    }

    const startTime = performance.now();

    this.renderState.isRendering = true;
    this.renderState.pendingRender = false;

    try {
      // 0. 카운트 + 타임아웃 cleanup (5초 쓰로틀, 필터 변경 시 즉시)
      this.updateLiveShipCountsThrottled();

      // 1. 스토어에서 전체 선박 가져오기
      const { features } = useShipStore.getState();
      const allShips = Array.from(features.values());

      // 2. 필터 캐시 생성 (렌더링 시작 시 1회)
      this.cache.filterCache = buildFilterCache();

      // 3. 뷰포트 범위 필터링 (먼저 수행 - 대량 데이터 감소)
      const viewportShips = this.viewportBounds
        ? filterByViewport(allShips, this.viewportBounds)
        : allShips;

      // 3.5 반경 필터링 (추적 모드 활성화 시)
      const radiusState = getRadiusFilterState();
      const radiusFilteredShips = filterByRadius(viewportShips, radiusState);

      // 4. 필터 적용 (캐시된 필터 사용 - O(1) lookup)
      const filteredShips = radiusFilteredShips.filter((ship) =>
        applyFilterWithCache(ship, this.cache.filterCache)
      );

      // 5. 밀도 제한 적용 (선박 아이콘 클러스터링, 우선순위 기반)
      const zoom = this.renderState.currentZoom;
      const densityLimitedShips = applyDensityLimit(filteredShips, zoom, this.cache.favoriteSet);

      // 6. 렌더링 트리거 증가
      this.cache.lastRenderTrigger++;

      // 7. 콜백 호출 (밀도 제한 적용된 데이터 = 아이콘 + 라벨 공통)
      // 아이콘이 표시되는 선박에만 라벨/신호상태도 표시
      this.onRenderCallback(densityLimitedShips, this.cache.lastRenderTrigger);

      // 8. 캐시 업데이트
      this.cache.lastShipsData = densityLimitedShips;
      this.cache.lastFilteredCount = filteredShips.length;

      // 9. 렌더링 시간 측정 및 간격 조정
      const renderTime = performance.now() - startTime;
      this.adjustRenderInterval(renderTime);

    } catch (error) {
      console.error('[ShipBatchRenderer] Render error:', error);
    } finally {
      this.renderState.isRendering = false;
      this.renderState.lastRenderTime = Date.now();
      this.renderState.animationFrameId = null;

      // 대기 중인 렌더링 요청이 있으면 다시 스케줄
      if (this.renderState.pendingRender) {
        this.scheduleRender();
      }
    }
  }

  /**
   * 적응형 렌더링 간격 조정
   * 줌 레벨에 따른 최소 간격 + 성능 기반 적응형 조정
   *
   * @param {number} renderTime - 렌더링 소요 시간 (ms)
   */
  adjustRenderInterval(renderTime) {
    const { targetRenderTime, maxRenderTime, maxInterval } = RENDER_CONFIG;
    const minInterval = getMinIntervalByZoom(this.renderState.currentZoom);

    if (renderTime > maxRenderTime) {
      // 렌더링 시간이 너무 길면 간격 증가 (최대 5초)
      this.renderState.currentInterval = Math.min(
        this.renderState.currentInterval * 1.2,
        maxInterval
      );
    } else if (renderTime < targetRenderTime) {
      // 렌더링 시간이 충분히 짧으면 간격 감소 (줌 레벨별 최소값까지)
      this.renderState.currentInterval = Math.max(
        this.renderState.currentInterval * 0.9,
        minInterval
      );
    }
  }

  /**
   * 카운트 + 타임아웃 cleanup (5초 쓰로틀, 필터 변경 시 즉시)
   * 참조: mda-react-front/src/common/deck.ts - updateLayerData 내 카운트 로직
   */
  updateLiveShipCountsThrottled() {
    const now = Date.now();
    const currentFilterHash = generateFilterHash();
    const filterChanged = currentFilterHash !== liveCountCache.lastFilterHash;
    const throttleExpired = now - liveCountCache.lastCalcTime >= LIVE_COUNT_THROTTLE_MS;

    if (!filterChanged && !throttleExpired) return;

    const result = calculateAndCleanupLiveShips();

    liveCountCache.lastCalcTime = now;
    liveCountCache.lastFilterHash = currentFilterHash;

    // cleanup 적용
    if (result.deleteIds.length > 0 || result.darkSignalConvertIds.length > 0) {
      useShipStore.getState().applyCleanup(result.deleteIds, result.darkSignalConvertIds);
    }

    // 카운트 업데이트 (값 비교 가드 — 불필요한 리렌더 방지)
    const state = useShipStore.getState();
    const prevKindCounts = state.kindCounts;
    const countsChanged = result.totalCount !== state.totalCount
      || result.darkSignalCount !== state.darkSignalCount
      || Object.keys(result.kindCounts).some(k => result.kindCounts[k] !== prevKindCounts[k]);

    if (countsChanged) {
      useShipStore.setState({
        kindCounts: result.kindCounts,
        totalCount: result.totalCount,
        darkSignalCount: result.darkSignalCount,
      });
    }
  }

  /**
   * 강제 렌더링 (필터 변경 등)
   * 일반 렌더링 주기에 따름
   */
  forceRender() {
    this.cache.filterCache = null;
    this.requestRender();
  }

  /**
   * 즉시 렌더링 (필터/선명표시 토글 등 사용자 인터랙션)
   * 렌더링 주기를 무시하고 즉시 실행
   */
  immediateRender() {
    // 기존 스케줄 취소
    if (this.renderState.animationFrameId) {
      clearTimeout(this.renderState.animationFrameId);
      cancelAnimationFrame(this.renderState.animationFrameId);
      this.renderState.animationFrameId = null;
    }

    // 캐시 클리어
    this.cache.filterCache = null;
    this.renderState.pendingRender = false;

    // 즉시 렌더링 실행
    requestAnimationFrame(() => {
      this.executeRender();
    });
  }

  /**
   * 캐시 클리어
   */
  clearCache() {
    this.cache.filterCache = null;
    this.cache.lastFilterHash = '';
    this.cache.lastShipsData = [];
    this.cache.lastFilteredCount = 0;
    this.cache.favoriteSet = null;
  }

  /**
   * 배치 렌더러 정리
   */
  dispose() {
    if (this.renderState.animationFrameId) {
      cancelAnimationFrame(this.renderState.animationFrameId);
      clearTimeout(this.renderState.animationFrameId);
      this.renderState.animationFrameId = null;
    }
    this.clearCache();
    this.onRenderCallback = null;
    console.log('[ShipBatchRenderer] Disposed');
  }

  /**
   * 마지막 필터링된 선박 데이터 반환
   * @returns {Array} 필터링된 선박 배열
   */
  getFilteredShips() {
    return this.cache.lastShipsData;
  }

  /**
   * 현재 렌더링 통계 반환
   * @returns {Object} 렌더링 통계
   */
  getStats() {
    const zoom = this.renderState.currentZoom;
    const densityConfig = getDensityConfig(zoom);
    return {
      currentZoom: zoom,
      minInterval: getMinIntervalByZoom(zoom),
      currentInterval: this.renderState.currentInterval,
      lastRenderTime: this.renderState.lastRenderTime,
      filteredCount: this.cache.lastFilteredCount,       // 밀도 제한 전 선박 수
      renderedCount: this.cache.lastShipsData.length,    // 밀도 제한 후 렌더링 선박 수
      densityConfig: {
        maxPerCell: densityConfig.maxPerCell,
        gridSizeMultiplier: densityConfig.gridSizeMultiplier,
      },
      renderTrigger: this.cache.lastRenderTrigger,
    };
  }
}

// 싱글톤 인스턴스
export const shipBatchRenderer = new ShipBatchRenderer();

// 유틸리티 함수 export
export { buildFilterCache, applyFilterWithCache, filterByViewport, applyDensityLimit, getDensityConfig };

export default ShipBatchRenderer;
