/**
 * HNS 대기확산 모델 엔진 (TypeScript)
 * ──────────────────────────────────────
 * hns_dispersion.py 직접 포팅
 *
 * 지원 모델:
 *   1. Gaussian Plume  — 연속 누출 (정상상태)
 *   2. Gaussian Puff   — 순간 누출 (시간변화)
 *   3. Dense Gas       — 고밀도 가스 (Britter-McQuaid)
 */
import type {
  StabilityClass,
  MeteoParams,
  SourceParams,
  SimParams,
  DispersionPoint,
  DispersionGridResult,
  ComputeDispersionParams,
  AeglDistances,
  AeglAreas,
  AlgorithmType,
} from './dispersionTypes';
import { getSubstanceToxicity } from './toxicityData';

// ──────────────────────────────────────
// 알고리즘별 Pasquill-Gifford 확산계수
// [ay, by, az, bz]
// σy = ay * x / sqrt(1 + by * x)
// σz = az * x / sqrt(1 + bz * x)
// ──────────────────────────────────────

/** ALOHA (EPA) — Briggs (1973) open-country */
const PG_ALOHA: Record<StabilityClass, [number, number, number, number]> = {
  A: [0.22, 0.0001, 0.2, 0.0],
  B: [0.16, 0.0001, 0.12, 0.0],
  C: [0.11, 0.0001, 0.08, 0.0002],
  D: [0.08, 0.0001, 0.06, 0.0015],
  E: [0.06, 0.0001, 0.03, 0.0003],
  F: [0.04, 0.0001, 0.016, 0.0003],
};

/** Gaussian Plume — Turner (1970) Pasquill-Gifford 표준 계수 (더 넓은 확산) */
const PG_GAUSSIAN: Record<StabilityClass, [number, number, number, number]> = {
  A: [0.25, 0.0001, 0.23, 0.0],
  B: [0.19, 0.0001, 0.16, 0.0],
  C: [0.13, 0.0001, 0.1, 0.0002],
  D: [0.09, 0.0001, 0.07, 0.0015],
  E: [0.07, 0.0001, 0.04, 0.0003],
  F: [0.05, 0.0001, 0.02, 0.0003],
};

/** CAMEO — ALOHA와 동일 엔진 (NOAA/EPA CAMEO Suite) */
const PG_CAMEO = PG_ALOHA;

/** AERMOD — Schulman-Scire urban 계수 (도시 지역, 더 넓은 확산) */
const PG_AERMOD: Record<StabilityClass, [number, number, number, number]> = {
  A: [0.32, 0.0004, 0.24, 0.001],
  B: [0.22, 0.0004, 0.18, 0.001],
  C: [0.16, 0.0004, 0.13, 0.001],
  D: [0.11, 0.0004, 0.09, 0.001],
  E: [0.08, 0.0004, 0.05, 0.001],
  F: [0.06, 0.0004, 0.03, 0.001],
};

/** 알고리즘별 PG 파라미터 맵 */
const PG_BY_ALGO: Record<string, Record<StabilityClass, [number, number, number, number]>> = {
  'ALOHA (EPA)': PG_ALOHA,
  CAMEO: PG_CAMEO,
  'Gaussian Plume': PG_GAUSSIAN,
  AERMOD: PG_AERMOD,
};

function getPgParams(
  algorithm?: AlgorithmType,
): Record<StabilityClass, [number, number, number, number]> {
  return PG_BY_ALGO[algorithm || 'ALOHA (EPA)'] || PG_ALOHA;
}

/** 수평 확산계수 σy (m) */
function sigmaY(x: number, stability: StabilityClass, algorithm?: AlgorithmType): number {
  const pg = getPgParams(algorithm);
  const [ay, by] = pg[stability];
  const xc = Math.max(x, 1.0);
  return (ay * xc) / Math.sqrt(1.0 + by * xc);
}

/** 수직 확산계수 σz (m) */
function sigmaZ(x: number, stability: StabilityClass, algorithm?: AlgorithmType): number {
  const pg = getPgParams(algorithm);
  const [, , az, bz] = pg[stability];
  const xc = Math.max(x, 1.0);
  return (az * xc) / Math.sqrt(1.0 + bz * xc);
}

/**
 * 기상 풍향 → 수학 좌표 회전각 변환
 * 기상 풍향 270° (서풍) → 바람이 동쪽(+x)으로 진행
 */
function windRotation(windDirDeg: number): [number, number] {
  const mathAngle = ((270.0 - windDirDeg) * Math.PI) / 180.0;
  return [Math.cos(mathAngle), Math.sin(mathAngle)];
}

/** 절대 좌표 → 바람 중심축 좌표 (xw=풍하, yw=횡풍) */
function rotateToWind(
  x: number,
  y: number,
  x0: number,
  y0: number,
  cosT: number,
  sinT: number,
): [number, number] {
  const dx = x - x0;
  const dy = y - y0;
  const xw = dx * cosT + dy * sinT;
  const yw = -dx * sinT + dy * cosT;
  return [xw, yw];
}

/** 미터 오프셋 → 경위도 변환 */
function metersToLonLat(
  xMeters: number,
  yMeters: number,
  originLon: number,
  originLat: number,
): [number, number] {
  const latRad = (originLat * Math.PI) / 180;
  const dLat = yMeters / 111320;
  const dLon = xMeters / (111320 * Math.cos(latRad));
  return [originLon + dLon, originLat + dLat];
}

/** g/m³ → ppm 변환 */
function gm3ToPpm(cGm3: number, mw: number, tempK: number, pressurePa: number): number {
  return ((cGm3 * (8.314 * tempK)) / (mw * pressurePa)) * 1e6;
}

// ──────────────────────────────────────
// Model 1: Gaussian Plume (연속 누출, 정상상태)
// ──────────────────────────────────────
function gaussianPlume(
  xArr: Float64Array,
  yArr: Float64Array,
  meteo: MeteoParams,
  source: SourceParams,
  sim: SimParams,
  algorithm?: AlgorithmType,
): Float64Array {
  const nx = xArr.length;
  const ny = yArr.length;
  const C = new Float64Array(ny * nx);

  const [cosT, sinT] = windRotation(meteo.windDirDeg);
  const u = Math.max(meteo.windSpeed, 0.5);
  const H = source.z0;
  const z = sim.zRef;
  const TWO_PI = 2.0 * Math.PI;

  for (let j = 0; j < ny; j++) {
    for (let i = 0; i < nx; i++) {
      const [xw, yw] = rotateToWind(xArr[i], yArr[j], source.x0, source.y0, cosT, sinT);

      if (xw <= 0) continue;

      const sy = sigmaY(xw, meteo.stability, algorithm);
      let sz = sigmaZ(xw, meteo.stability, algorithm);
      sz = Math.min(sz, meteo.mixingHeight);

      const termY = Math.exp(-0.5 * (yw / sy) ** 2);
      const termZ1 = Math.exp(-0.5 * ((z - H) / sz) ** 2);
      const termZ2 = Math.exp(-0.5 * ((z + H) / sz) ** 2);

      C[j * nx + i] = (source.Q / (TWO_PI * sy * sz * u)) * termY * (termZ1 + termZ2);
    }
  }

  return C;
}

// ──────────────────────────────────────
// Model 2: Gaussian Puff (순간 누출, 시간변화)
// ──────────────────────────────────────
function gaussianPuff(
  xArr: Float64Array,
  yArr: Float64Array,
  meteo: MeteoParams,
  source: SourceParams,
  sim: SimParams,
  t: number,
  algorithm?: AlgorithmType,
): Float64Array {
  const nx = xArr.length;
  const ny = yArr.length;
  const C = new Float64Array(ny * nx);

  if (t <= 0) return C;

  const u = Math.max(meteo.windSpeed, 0.5);
  const [cosT, sinT] = windRotation(meteo.windDirDeg);

  // 퍼프 중심 이동
  const xc = source.x0 + u * t * cosT;
  const yc = source.y0 + u * t * sinT;

  // 이동거리 기준으로 σ 계산
  const travelDist = u * t;
  const sy = sigmaY(travelDist, meteo.stability, algorithm);
  let sz = sigmaZ(travelDist, meteo.stability, algorithm);
  const sx = sy; // 풍하 방향 확산 ≈ 횡풍 방향
  sz = Math.min(sz, meteo.mixingHeight);

  const H = source.z0;
  const z = sim.zRef;
  const norm = Math.pow(2 * Math.PI, 1.5) * sx * sy * sz;

  for (let j = 0; j < ny; j++) {
    for (let i = 0; i < nx; i++) {
      const dx = xArr[i] - xc;
      const dy = yArr[j] - yc;

      const termX = Math.exp(-0.5 * (dx / sx) ** 2);
      const termY = Math.exp(-0.5 * (dy / sy) ** 2);
      const termZ = Math.exp(-0.5 * ((z - H) / sz) ** 2) + Math.exp(-0.5 * ((z + H) / sz) ** 2);

      C[j * nx + i] = (source.QTotal / norm) * termX * termY * termZ;
    }
  }

  return C;
}

// ──────────────────────────────────────
// Model 3: Dense Gas (Britter-McQuaid)
// ──────────────────────────────────────
function denseGasBritterMcquaid(
  xArr: Float64Array,
  yArr: Float64Array,
  meteo: MeteoParams,
  source: SourceParams,
  _sim: SimParams,
  t: number,
): Float64Array {
  const nx = xArr.length;
  const ny = yArr.length;
  const C = new Float64Array(ny * nx);

  const rhoAir = (meteo.pressure * 0.02897) / (8.314 * meteo.temperature);
  const g = 9.81;
  const u = Math.max(meteo.windSpeed, 0.5);
  const rhoG = source.densityGas;
  const C0Vol = rhoG / (rhoG + rhoAir);
  const gPrime0 = (g * (rhoG - rhoAir)) / rhoAir;

  const [cosT, sinT] = windRotation(meteo.windDirDeg);

  for (let j = 0; j < ny; j++) {
    for (let i = 0; i < nx; i++) {
      const [xw, yw] = rotateToWind(xArr[i], yArr[j], source.x0, source.y0, cosT, sinT);

      if (xw <= 0) continue;

      let cVol: number;

      if (source.releaseDuration === 0) {
        // 순간 누출 (Puff)
        const xCenter = u * t;
        const V0 = Math.PI * source.poolRadius ** 2 * 1.0;
        const r0 = source.poolRadius;
        const rT = r0 + 1.1 * Math.pow(gPrime0 * V0, 0.25) * Math.pow(Math.max(t, 0.1), 0.5);
        const sigmaCloud = rT / 2.15;
        const distXr = xw - xCenter;

        cVol =
          C0Vol *
          Math.exp(-0.5 * (distXr / sigmaCloud) ** 2) *
          Math.exp(-0.5 * (yw / sigmaCloud) ** 2);
      } else {
        // 연속 누출 (Plume)
        const qv = source.Q / (rhoG * 1000);
        const gPrimeLine = (gPrime0 * qv) / u;
        const b0 = source.poolRadius;
        const bX = Math.max(
          b0 + 2.5 * Math.pow(gPrimeLine / u ** 2, 0.333) * Math.pow(xw, 0.6),
          b0,
        );

        cVol = C0Vol * (b0 / bX) * Math.exp(-0.5 * (yw / (bX / 2)) ** 2);
      }

      // 부피분율 → g/m³
      const MW = source.molecularWeight;
      C[j * nx + i] = (cVol * (MW * meteo.pressure)) / (8.314 * meteo.temperature);
    }
  }

  return C;
}

// ──────────────────────────────────────
// 통합 계산 함수
// ──────────────────────────────────────
export function computeDispersion(params: ComputeDispersionParams): DispersionGridResult {
  const { meteo, source, sim, modelType, originLon, originLat, substanceName, t, algorithm } =
    params;

  // 격자 생성
  const xArr = new Float64Array(sim.nx);
  const yArr = new Float64Array(sim.ny);
  for (let i = 0; i < sim.nx; i++) {
    xArr[i] = sim.xRange[0] + ((sim.xRange[1] - sim.xRange[0]) * i) / (sim.nx - 1);
  }
  for (let j = 0; j < sim.ny; j++) {
    yArr[j] = sim.yRange[0] + ((sim.yRange[1] - sim.yRange[0]) * j) / (sim.ny - 1);
  }

  // 모델별 농도 계산 (g/m³)
  let cGm3: Float64Array;
  switch (modelType) {
    case 'plume':
      cGm3 = gaussianPlume(xArr, yArr, meteo, source, sim, algorithm);
      break;
    case 'puff':
      cGm3 = gaussianPuff(xArr, yArr, meteo, source, sim, t, algorithm);
      break;
    case 'dense_gas':
      cGm3 = denseGasBritterMcquaid(xArr, yArr, meteo, source, sim, t);
      break;
  }

  // 독성 데이터
  const tox = getSubstanceToxicity(substanceName);

  // g/m³ → ppm 변환 + 지도 좌표 변환
  const points: DispersionPoint[] = [];
  let maxConcentration = 0;

  // AEGL 거리/면적 계산용
  const cellAreaM2 =
    ((sim.xRange[1] - sim.xRange[0]) / sim.nx) * ((sim.yRange[1] - sim.yRange[0]) / sim.ny);
  let aegl1Cells = 0,
    aegl2Cells = 0,
    aegl3Cells = 0;
  let aegl1MaxDist = 0,
    aegl2MaxDist = 0,
    aegl3MaxDist = 0;

  const [cosT, sinT] = windRotation(meteo.windDirDeg);

  for (let j = 0; j < sim.ny; j++) {
    for (let i = 0; i < sim.nx; i++) {
      const idx = j * sim.nx + i;
      const ppm = gm3ToPpm(cGm3[idx], tox.mw, meteo.temperature, meteo.pressure);

      if (ppm < 0.01) continue;

      if (ppm > maxConcentration) maxConcentration = ppm;

      // 미터 좌표 → 경위도
      const [lon, lat] = metersToLonLat(xArr[i], yArr[j], originLon, originLat);
      points.push({ lon, lat, concentration: ppm });

      // 풍하 거리 계산
      const [xw] = rotateToWind(xArr[i], yArr[j], source.x0, source.y0, cosT, sinT);
      const dist = Math.max(xw, 0);

      // AEGL 거리/면적 집계
      if (ppm >= tox.aegl1) {
        aegl1Cells++;
        if (dist > aegl1MaxDist) aegl1MaxDist = dist;
      }
      if (ppm >= tox.aegl2) {
        aegl2Cells++;
        if (dist > aegl2MaxDist) aegl2MaxDist = dist;
      }
      if (ppm >= tox.aegl3) {
        aegl3Cells++;
        if (dist > aegl3MaxDist) aegl3MaxDist = dist;
      }
    }
  }

  const aeglDistances: AeglDistances = {
    aegl1: Math.round(aegl1MaxDist),
    aegl2: Math.round(aegl2MaxDist),
    aegl3: Math.round(aegl3MaxDist),
  };

  const aeglAreas: AeglAreas = {
    aegl1: parseFloat(((aegl1Cells * cellAreaM2) / 1e6).toFixed(2)),
    aegl2: parseFloat(((aegl2Cells * cellAreaM2) / 1e6).toFixed(2)),
    aegl3: parseFloat(((aegl3Cells * cellAreaM2) / 1e6).toFixed(2)),
  };

  return {
    points,
    maxConcentration: parseFloat(maxConcentration.toFixed(2)),
    aeglDistances,
    aeglAreas,
    modelType,
    timeStep: t,
    substance: substanceName,
  };
}
