/**
 * 드론 영상 ↔ 지리정보 매핑 서비스
 *
 * 드론 CSV(frame별 위경도/자세) + POI/측점 CSV를 조합하여:
 *  - 건물/측점명 → 해당 프레임 탐색
 *  - 프레임 번호 → 카메라 시야 내 POI/측점 목록
 */

import fs from 'fs';
import path from 'path';
import iconv from 'iconv-lite';

// ──────────────────────────────────────────
// 타입 정의
// ──────────────────────────────────────────

export interface DroneFrame {
  frame: number;
  lat: number;
  lon: number;
  altitude: number;
  yaw: number;    // 기수방향 (North=0, 시계방향, degrees)
  pitch: number;  // 카메라 틸트 (음수=아래, degrees)
  roll: number;
  focalLen: number; // 35mm 환산 초점거리 (mm)
}

export interface GeoPoint {
  title: string;
  category: string;
  lat: number;
  lon: number;
  z: number;       // 표고 (m)
  type: 'poi' | 'station';
}

export interface FrameMatch {
  frame: number;
  time: number;      // 초 단위 (frame / fps)
  bearingDiff: number;   // 수평 각도차 (degrees, |값|이 작을수록 중심에 가까움)
  elevationDiff: number; // 수직 각도차 (degrees)
  distance: number;      // 수평 거리 (m)
  pixelX: number;   // 이미지 내 추정 픽셀 X (0~1 정규화)
  pixelY: number;   // 이미지 내 추정 픽셀 Y (0~1 정규화)
}

export interface PoiInFrame {
  poi: GeoPoint;
  bearingDiff: number;
  elevationDiff: number;
  distance: number;
  pixelX: number;
  pixelY: number;
}

// ──────────────────────────────────────────
// 카메라 파라미터 (35mm 환산, Full Frame 36×24mm 기준)
// ──────────────────────────────────────────
const SENSOR_W_MM = 36;
const SENSOR_H_MM = 24;
const R_EARTH = 6371000;

function toRad(deg: number) { return deg * Math.PI / 180; }

// ──────────────────────────────────────────
// 블렌더 방식 월드 ENU 좌표계
//
// 블렌더에서 하는 것과 동일:
//   1. 기준점(origin) = 드론 첫 프레임 위치
//   2. 모든 측점 / 드론경로를 이 기준점 기준 ENU(m)로 단 한 번 변환
//   3. 카메라(=드론) 위치도 같은 월드 좌표
//   4. 측점 벡터 = 측점_월드 − 카메라_월드  →  카메라 회전 적용  →  투영
//
// cos(lat) 보정을 기준점의 위도로 고정 → 전 프레임 일관성 보장
// ──────────────────────────────────────────

/** 위경도+표고 → 월드 ENU (m). refLat/refLon/refAlt = 원점 */
function geoToEnu(
  lat: number, lon: number, alt: number,
  refLat: number, refLon: number, refAlt: number,
): [number, number, number] {
  const cosRef = Math.cos(toRad(refLat));
  const e = toRad(lon - refLon) * cosRef * R_EARTH;   // East  (m)
  const n = toRad(lat - refLat) * R_EARTH;             // North (m)
  const u = alt - refAlt;                              // Up    (m)
  return [e, n, u];
}

/** 월드 ENU 벡터 + 카메라 자세 → 정규화 픽셀 (0~1) */
function projectEnu(
  relEnu: [number, number, number],   // 측점_월드 − 카메라_월드 (ENU m)
  yawDeg: number,                     // 카메라 yaw (North=0, CW+, degrees)
  pitchDeg: number,                   // 카메라 pitch (음수=아래, degrees)
  focalMm: number,                    // 35mm 환산 초점거리
  yawOffset = 0,
): { px: number; py: number; cx: number; cy: number; cz: number } | null {
  const yaw   = toRad(yawDeg + yawOffset);
  const pitch = toRad(pitchDeg);
  const cosY = Math.cos(yaw), sinY = Math.sin(yaw);
  const cosP = Math.cos(pitch), sinP = Math.sin(pitch);

  // 카메라 축 벡터 (ENU 기준)
  const fwd:   readonly [number, number, number] = [sinY * cosP, cosY * cosP, sinP];
  const right: readonly [number, number, number] = [cosY, -sinY, 0];
  const up:    readonly [number, number, number] = [
    right[1] * fwd[2] - right[2] * fwd[1],
    right[2] * fwd[0] - right[0] * fwd[2],
    right[0] * fwd[1] - right[1] * fwd[0],
  ];

  const dot = (a: readonly [number, number, number], b: readonly [number, number, number]) =>
    a[0] * b[0] + a[1] * b[1] + a[2] * b[2];

  const cx = dot(relEnu, right);  // 카메라 좌우 (+= 오른쪽)
  const cy = dot(relEnu, up);     // 카메라 상하 (+= 위)
  const cz = dot(relEnu, fwd);    // 카메라 깊이 (+= 앞)

  if (cz <= 0) return null;       // 카메라 뒤 → 불가

  const f = focalMm || 24;
  const px = 0.5 + (cx / cz) * (f / SENSOR_W_MM);
  const py = 0.5 - (cy / cz) * (f / SENSOR_H_MM);
  return { px, py, cx, cy, cz };
}

/** 편의 래퍼: DroneFrame + POI + 공통 기준점 → 픽셀 */
function project3D(
  drone: DroneFrame,
  poi: { lat: number; lon: number; z: number },
  yawOffset = 0,
  origin?: { lat: number; lon: number; alt: number },
): { px: number; py: number; dist: number; h: number; v: number; inFov: boolean } | null {
  // 기준점: 지정하지 않으면 현재 드론 위치 사용 (기존 동작 유지)
  const ref = origin ?? { lat: drone.lat, lon: drone.lon, alt: drone.altitude };

  const stEnu = geoToEnu(poi.lat, poi.lon, poi.z, ref.lat, ref.lon, ref.alt);
  const drEnu = geoToEnu(drone.lat, drone.lon, drone.altitude, ref.lat, ref.lon, ref.alt);
  const relEnu: [number, number, number] = [
    stEnu[0] - drEnu[0],
    stEnu[1] - drEnu[1],
    stEnu[2] - drEnu[2],
  ];
  const dist = Math.sqrt(relEnu[0] ** 2 + relEnu[1] ** 2);  // 수평 거리

  const res = projectEnu(relEnu, drone.yaw, drone.pitch, drone.focalLen || 24, yawOffset);
  if (!res) return null;

  const h = Math.atan2(res.cx, res.cz) * (180 / Math.PI);
  const v = Math.atan2(res.cy, res.cz) * (180 / Math.PI);
  const inFov = res.px >= 0 && res.px <= 1 && res.py >= 0 && res.py <= 1;
  return {
    px: Math.max(0, Math.min(1, res.px)),
    py: Math.max(0, Math.min(1, res.py)),
    dist, h, v, inFov,
  };
}

// ──────────────────────────────────────────
// CSV 파싱
// ──────────────────────────────────────────

function parseCsvLine(line: string): string[] {
  const result: string[] = [];
  let current = '';
  let inQuotes = false;
  for (const ch of line) {
    if (ch === '"') { inQuotes = !inQuotes; }
    else if (ch === ',' && !inQuotes) { result.push(current.trim()); current = ''; }
    else { current += ch; }
  }
  result.push(current.trim());
  return result;
}

function readCsvUtf8(filePath: string): string[][] {
  const raw = fs.readFileSync(filePath);
  // UTF-8 BOM 체크
  const hasBom = raw[0] === 0xEF && raw[1] === 0xBB && raw[2] === 0xBF;
  let text: string;
  if (hasBom) {
    text = raw.slice(3).toString('utf-8');
  } else {
    try {
      text = iconv.decode(raw, 'euc-kr');
      if (!/[가-힣]/.test(text)) text = raw.toString('utf-8');
    } catch {
      text = raw.toString('utf-8');
    }
  }
  // BOM 문자 제거 및 파싱
  return text.replace(/^\uFEFF/, '').split(/\r?\n/).filter(Boolean).map(parseCsvLine);
}

// ──────────────────────────────────────────
// 데이터 로더 (싱글턴 캐시)
// ──────────────────────────────────────────

let _frames: DroneFrame[] | null = null;
let _pois: GeoPoint[] | null = null;
let _dataDir: string | null = null;
let _terrainOffset: number | null = null; // abs_alt - rel_alt (지형 해발고도 m)

/**
 * SRT 파일에서 첫 프레임의 rel_alt(AGL)와 abs_alt(AMSL)를 읽어
 * terrain offset = abs_alt - rel_alt 를 반환.
 * 이 값을 드론 altitude(abs_alt)에서 빼면 AGL 고도가 됨.
 */
function loadTerrainOffset(dataDir: string): number {
  const srtFiles = fs.readdirSync(dataDir).filter(f => f.endsWith('.srt'));
  if (!srtFiles.length) return 0;
  const srtPath = path.join(dataDir, srtFiles[0]);
  const raw = fs.readFileSync(srtPath);
  const text = iconv.decode(raw, 'utf-8').slice(0, 2000); // 첫 부분만 읽기
  const relMatch = text.match(/rel_alt:\s*([\d.]+)/);
  const absMatch = text.match(/abs_alt:\s*([\d.]+)/);
  if (relMatch && absMatch) {
    const offset = parseFloat(absMatch[1]) - parseFloat(relMatch[1]);
    console.log(`[geo] terrain offset: ${offset.toFixed(2)}m (abs=${absMatch[1]}, rel=${relMatch[1]})`);
    return offset;
  }
  return 0;
}

// ──────────────────────────────────────────
// 선로 중심선 (center.csv, 224점)
// ──────────────────────────────────────────

export interface CenterlinePoint {
  lat: number;
  lon: number;
  z: number;  // 타원체고(h) — GPS 표고 (m), SRT abs_alt와 동일 기준
}

let _centerline: CenterlinePoint[] | null = null;

function loadCenterline(): CenterlinePoint[] {
  if (_centerline) return _centerline;

  const csvPath = process.env.CENTER_CSV_PATH ??
    path.resolve(__dirname, '../../../pythonsource/input/center.csv');

  if (!fs.existsSync(csvPath)) {
    console.warn(`[geo] center.csv not found: ${csvPath}`);
    _centerline = [];
    return _centerline;
  }

  const rows = readCsvUtf8(csvPath);
  if (rows.length < 2) { _centerline = []; return _centerline; }

  // 컬럼 순서 (EUC-KR 헤더 파싱 우회, 인덱스 직접 사용):
  //  0=id, 1=lat, 2=lon, 3=표고(H), 4=지오이드높이(N), 5=타원체고(h), ..., 9=x(5186), 10=y(5186)
  _centerline = rows.slice(1).map(r => ({
    lat: parseFloat(r[1]),
    lon: parseFloat(r[2]),
    z:   parseFloat(r[5]),  // 타원체고(h)
  })).filter(p => !isNaN(p.lat) && !isNaN(p.lon) && !isNaN(p.z));

  console.log(`[geo] Centerline: ${_centerline.length} points from ${path.basename(csvPath)}`);
  return _centerline;
}

export function getCenterlinePoints(): CenterlinePoint[] {
  return loadCenterline();
}

export function setGeoDataDir(dir: string) {
  _dataDir = dir;
  _frames = null;
  _pois = null;
  _terrainOffset = null;
  _centerline = null;
}

function loadFrames(): DroneFrame[] {
  if (_frames) return _frames;
  if (!_dataDir) return [];

  const csvPath = fs.readdirSync(_dataDir)
    .find(f => f.endsWith('.csv') && !fs.statSync(path.join(_dataDir!, f)).isDirectory()
      && !path.join(_dataDir!, f).includes('building'));

  // 드론 CSV는 data dir 바로 아래 (building 폴더 제외)
  const files = fs.readdirSync(_dataDir).filter(f =>
    f.endsWith('.csv') && f.includes('회덕') && !f.includes('POI') && !f.includes('측점')
  );
  if (!files.length) return [];

  const rows = readCsvUtf8(path.join(_dataDir, files[0]));
  const header = rows[0].map(h => h.trim().replace(/^\uFEFF/, ''));
  const fi = (name: string) => header.indexOf(name);

  _frames = rows.slice(1).map(r => ({
    frame: parseInt(r[fi('frame_cnt')], 10),
    lat: parseFloat(r[fi('latitude')]),
    lon: parseFloat(r[fi('longitude')]),
    altitude: parseFloat(r[fi('altitude')]),
    yaw: parseFloat(r[fi('yaw')]),
    pitch: parseFloat(r[fi('pitch')]),
    roll: parseFloat(r[fi('roll')]),
    focalLen: parseFloat(r[fi('focal_len')]),
  })).filter(f => !isNaN(f.lat));

  return _frames;
}

function loadPois(): GeoPoint[] {
  if (_pois) return _pois;
  if (!_dataDir) return [];

  const buildingDir = path.join(_dataDir, 'building');
  if (!fs.existsSync(buildingDir)) return [];

  const result: GeoPoint[] = [];

  // POI 위경도 파일 (_타원체고 버전 우선)
  const allPoiFiles = fs.readdirSync(buildingDir).filter(f => f.includes('POI') && f.includes('위경도'));
  const poiFiles = allPoiFiles.filter(f => f.includes('타원체고')).length > 0
    ? allPoiFiles.filter(f => f.includes('타원체고'))
    : allPoiFiles.filter(f => !f.includes('타원체고'));
  for (const f of poiFiles) {
    const rows = readCsvUtf8(path.join(buildingDir, f));
    const header = rows[0].map((h: string) => h.trim().replace(/^\uFEFF/, ''));
    const fi = (name: string) => header.indexOf(name);
    for (const r of rows.slice(1)) {
      const lat = parseFloat(r[fi('lat')]);
      const lon = parseFloat(r[fi('lon')]);
      if (isNaN(lat) || isNaN(lon)) continue;
      result.push({
        title: r[fi('title')] || '',
        category: r[fi('category_clean')] || '건물',
        lat, lon,
        z: parseFloat(r[fi('z')]) || 0,
        type: 'poi',
      });
    }
  }

  // 측점 위경도 파일
  const stationFiles = fs.readdirSync(buildingDir).filter(f => f.includes('측점') && f.includes('위경도'));
  for (const f of stationFiles) {
    const rows = readCsvUtf8(path.join(buildingDir, f));
    const header = rows[0].map((h: string) => h.trim().replace(/^\uFEFF/, ''));
    const fi = (name: string) => header.indexOf(name);
    for (const r of rows.slice(1)) {
      const lat = parseFloat(r[fi('lat')]);
      const lon = parseFloat(r[fi('lon')]);
      if (isNaN(lat) || isNaN(lon)) continue;
      result.push({
        title: r[fi('title')] || '',
        category: '측점',
        lat, lon,
        z: parseFloat(r[fi('z')]) || 0,
        type: 'station',
      });
    }
  }

  _pois = result;
  return _pois;
}

// ──────────────────────────────────────────
// 월드 좌표 원점: 첫 번째 측점
//
// 드론 frame[0]을 원점으로 쓰면 드론 GPS 오차가 그대로 반영됨.
// 대신 첫 번째 측점(측량 기준점)을 ENU 원점으로 사용하면
// 드론 카메라가 측점 좌표계에 맞춰 정렬됨.
// ──────────────────────────────────────────

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

/** 첫 번째 측점 위치를 ENU 원점으로 반환 */
function getWorldOrigin(frames: DroneFrame[], pois: GeoPoint[]): { lat: number; lon: number; alt: number } {
  const stations = pois.filter(p => p.type === 'station').sort((a, b) => stationOrder(a.title) - stationOrder(b.title));
  if (stations.length) {
    return { lat: stations[0].lat, lon: stations[0].lon, alt: stations[0].z };
  }
  // 측점 없으면 frame[0] fallback
  return frames[0]
    ? { lat: frames[0].lat, lon: frames[0].lon, alt: frames[0].altitude }
    : { lat: 0, lon: 0, alt: 0 };
}

// ──────────────────────────────────────────
// 핵심 API
// ──────────────────────────────────────────

const DEFAULT_FPS = 30;

/**
 * 건물/측점명 검색 → 매칭된 POI + 카메라 시야에 들어오는 프레임 목록
 * marginFactor: FOV 대비 허용 배수 (1.0 = 정확한 FOV 안, 1.5 = 50% 여유)
 */
export function findFramesForPoi(query: string, marginFactor = 1.0, maxDist = 2000, yawOffset = 0): {
  poi: GeoPoint | null;
  frames: FrameMatch[];
} {
  const frames = loadFrames();
  const pois = loadPois();

  const q = query.trim().toLowerCase();
  const poi = pois.find(p => p.title.toLowerCase().includes(q));
  if (!poi) return { poi: null, frames: [] };

  const matches: FrameMatch[] = [];

  // 첫 번째 측점을 ENU 원점으로 사용 (드론 카메라를 측점 좌표계에 정렬)
  const origin = getWorldOrigin(frames, pois);

  for (const f of frames) {
    const res = project3D(f, poi, yawOffset, origin);
    if (!res) continue;
    if (res.dist > maxDist) continue;

    // marginFactor: FOV 바깥까지 허용하는 배율
    const halfW = 0.5 * marginFactor;
    const halfH = 0.5 * marginFactor;
    const rawPx = 0.5 + (res.px - 0.5);  // 이미 0~1
    const rawPy = 0.5 + (res.py - 0.5);
    if (Math.abs(rawPx - 0.5) > halfW || Math.abs(rawPy - 0.5) > halfH) continue;

    matches.push({
      frame: f.frame,
      time: f.frame / DEFAULT_FPS,
      bearingDiff: res.h,
      elevationDiff: res.v,
      distance: res.dist,
      pixelX: res.px,
      pixelY: res.py,
    });
  }

  // 연속 프레임 그룹화: 끊김 없이 이어지는 구간에서 가장 중심에 가까운 프레임 1개만 선택
  matches.sort((a, b) => a.frame - b.frame);

  const GAP = 30; // 30프레임 이상 끊기면 새 구간
  const groups: FrameMatch[][] = [];
  let group: FrameMatch[] = [];

  for (const m of matches) {
    if (group.length === 0 || m.frame - group[group.length - 1].frame <= GAP) {
      group.push(m);
    } else {
      groups.push(group);
      group = [m];
    }
  }
  if (group.length > 0) groups.push(group);

  // 각 구간에서 가장 중심에 가까운 프레임 선택
  const best = groups.map(g => {
    // groupStart/groupEnd는 시간순 정렬 상태에서 먼저 기록
    const groupStart = g[0].frame;
    const groupEnd = g[g.length - 1].frame;
    g.sort((a, b) => (a.bearingDiff ** 2 + a.elevationDiff ** 2) - (b.bearingDiff ** 2 + b.elevationDiff ** 2));
    return { ...g[0], groupSize: g.length, groupStart, groupEnd };
  });

  // 거리 가까운 순 정렬
  best.sort((a, b) => (a.bearingDiff ** 2 + a.elevationDiff ** 2) - (b.bearingDiff ** 2 + b.elevationDiff ** 2));

  return { poi, frames: best as any };
}

/**
 * 프레임 번호 → 해당 프레임에서 카메라 시야에 들어오는 POI/측점 목록
 */
export function findPoisForFrame(frameNum: number, marginFactor = 1.0, yawOffset = 0): {
  droneFrame: DroneFrame | null;
  pois: PoiInFrame[];
} {
  const frames = loadFrames();
  const pois = loadPois();

  // 정확히 일치하는 프레임 우선, 없으면 가장 가까운 frame_cnt로 fallback
  const drone = frames.find(f => f.frame === frameNum) ?? (() => {
    let best = frames[0];
    let bestD = Math.abs((best?.frame ?? 0) - frameNum);
    for (const f of frames) {
      const d = Math.abs(f.frame - frameNum);
      if (d < bestD) { bestD = d; best = f; }
      if (d === 0) break;
    }
    return best;
  })();
  if (!drone) return { droneFrame: null, pois: [] };

  const result: PoiInFrame[] = [];

  // 첫 번째 측점을 ENU 원점으로 사용
  const origin = getWorldOrigin(frames, pois);

  for (const poi of pois) {
    const res = project3D(drone, poi, yawOffset, origin);
    if (!res) continue;

    // marginFactor 1.0 = 정확한 FOV, 1.3 = 30% 여유
    const halfW = 0.5 * marginFactor;
    const halfH = 0.5 * marginFactor;
    if (Math.abs(res.px - 0.5) > halfW || Math.abs(res.py - 0.5) > halfH) continue;

    result.push({ poi, bearingDiff: res.h, elevationDiff: res.v, distance: res.dist, pixelX: res.px, pixelY: res.py });
  }

  result.sort((a, b) => a.distance - b.distance);
  return { droneFrame: drone, pois: result };
}

/** POI 전체 목록 (검색/자동완성용) */
export function getAllPois(): GeoPoint[] {
  return loadPois();
}

/** 드론 비행경로 데이터 (경로 시각화용) */
export function getDroneFrames(): DroneFrame[] {
  return loadFrames();
}
