"""
WING HNS 대기확산 모델 알고리즘
=====================================
지원 모델:
  1. Gaussian Plume  - 연속 누출 (Continuous Release)
  2. Gaussian Puff   - 순간 누출 (Instantaneous Release)
  3. Dense Gas Model - 고밀도 가스 (ALOHA 방식, Britter-McQuaid)

출력:
  - 시간별 농도장 2D 히트맵 애니메이션 (MP4 / GIF)

의존 라이브러리:
  pip install numpy matplotlib scipy

작성: WING 프로젝트 / HNS 대응 모듈
"""

import numpy as np
import matplotlib.pyplot as plt
import matplotlib.animation as animation
from matplotlib.colors import LogNorm
from dataclasses import dataclass, field
from typing import Literal, Optional
import warnings
warnings.filterwarnings("ignore")


# ──────────────────────────────────────────────
# 1. 입력 파라미터 데이터 클래스
# ──────────────────────────────────────────────

@dataclass
class MeteoParams:
    """기상 조건"""
    wind_speed: float = 5.0          # 풍속 (m/s)
    wind_dir_deg: float = 270.0      # 풍향 (기상 기준, 0=북, 90=동, ...)
    stability: str = "D"             # Pasquill-Gifford 안정도 (A~F)
    temperature: float = 293.15      # 기온 (K)
    pressure: float = 101325.0       # 기압 (Pa)
    mixing_height: float = 800.0     # 혼합층 높이 (m)


@dataclass
class SourceParams:
    """누출원 정보"""
    Q: float = 10.0                   # 배출률 (g/s) - Plume용
    Q_total: float = 5000.0          # 총 누출량 (g)  - Puff용
    x0: float = 0.0                  # 누출 위치 X (m)
    y0: float = 0.0                  # 누출 위치 Y (m)
    z0: float = 0.5                  # 누출 높이 (m)  (해상 = 수면 근처)
    release_duration: float = 0.0    # 누출 지속시간 (s), 0=순간
    # Dense Gas 전용
    molecular_weight: float = 71.9   # 분자량 (g/mol) - 기본: Chlorine
    vapor_pressure: float = 670.0    # 증기압 (mmHg)
    density_gas: float = 3.2         # 가스 밀도 (kg/m³) at 누출 조건
    pool_radius: float = 5.0         # 액체풀 반경 (m)


@dataclass
class SimParams:
    """시뮬레이션 격자 및 시간 설정"""
    x_range: tuple = (-50, 3000)     # X 범위 (m)
    y_range: tuple = (-800, 800)     # Y 범위 (m)
    nx: int = 200                    # X 격자 수
    ny: int = 160                    # Y 격자 수
    z_ref: float = 1.5               # 농도 계산 기준 높이 (m, 호흡선)
    t_start: float = 0.0             # 시작 시간 (s)
    t_end: float = 600.0             # 종료 시간 (s)
    dt: float = 30.0                 # 시간 간격 (s)


# ──────────────────────────────────────────────
# 2. 확산계수 (Pasquill-Gifford σy, σz)
# ──────────────────────────────────────────────

# Briggs (1973) open-country 파라미터
_PG_PARAMS = {
    #  stability: (ay, by, az, bz) → σy = ay*x/(1+by*x)^0.5, σz = az*x/(1+bz*x)^0.5
    "A": (0.22, 0.0001, 0.20, 0.000),
    "B": (0.16, 0.0001, 0.12, 0.000),
    "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),
}


def sigma_y(x: np.ndarray, stability: str) -> np.ndarray:
    """수평 확산계수 σy (m)"""
    ay, by, _, _ = _PG_PARAMS[stability]
    x = np.maximum(x, 1.0)
    return ay * x / np.sqrt(1.0 + by * x)


def sigma_z(x: np.ndarray, stability: str) -> np.ndarray:
    """수직 확산계수 σz (m)"""
    _, _, az, bz = _PG_PARAMS[stability]
    x = np.maximum(x, 1.0)
    return az * x / np.sqrt(1.0 + bz * x)


# ──────────────────────────────────────────────
# 3. 바람 방향 회전 유틸리티
# ──────────────────────────────────────────────

def wind_rotation(wind_dir_deg: float):
    """
    기상 풍향 → 수학 좌표 회전각 변환
    Returns: (cos_theta, sin_theta)
    기상 풍향 270° (서풍) → 바람이 동쪽(+x)으로 진행
    """
    math_angle = np.radians(270.0 - wind_dir_deg)
    return np.cos(math_angle), np.sin(math_angle)


def rotate_to_wind(X, Y, x0, y0, cos_t, sin_t):
    """절대 좌표 → 바람 중심축 좌표 (x'=풍하, y'=횡풍)"""
    dx = X - x0
    dy = Y - y0
    xw =  dx * cos_t + dy * sin_t   # 풍하거리
    yw = -dx * sin_t + dy * cos_t   # 횡풍거리
    return xw, yw


# ──────────────────────────────────────────────
# 4. Model 1: Gaussian Plume (연속 누출)
# ──────────────────────────────────────────────

def gaussian_plume(
    X: np.ndarray, Y: np.ndarray,
    meteo: MeteoParams, src: SourceParams, sim: SimParams,
    t: float = None   # Plume은 정상 상태; t 인수는 인터페이스 통일용
) -> np.ndarray:
    """
    정상 상태 가우시안 플룸 농도 (g/m³)
    - 지면 반사 포함 (image method)
    - 혼합층 상한 반사 포함 (선택적)

    C(x,y,z) = Q / (2π σy σz u)
               × exp(-y²/2σy²)
               × [exp(-(z-H)²/2σz²) + exp(-(z+H)²/2σz²)]
    """
    cos_t, sin_t = wind_rotation(meteo.wind_dir_deg)
    xw, yw = rotate_to_wind(X, Y, src.x0, src.y0, cos_t, sin_t)

    # 풍하 양수 구역만 계산
    mask = xw > 0
    C = np.zeros_like(X)

    sy = sigma_y(xw[mask], meteo.stability)
    sz = sigma_z(xw[mask], meteo.stability)
    sz = np.minimum(sz, meteo.mixing_height)  # 혼합층 상한 클리핑

    u = max(meteo.wind_speed, 0.5)
    H = src.z0
    z = sim.z_ref

    # 지면 반사항
    term_y  = np.exp(-0.5 * (yw[mask] / sy)**2)
    term_z1 = np.exp(-0.5 * ((z - H) / sz)**2)
    term_z2 = np.exp(-0.5 * ((z + H) / sz)**2)

    C[mask] = (src.Q / (2 * np.pi * sy * sz * u)) * term_y * (term_z1 + term_z2)
    return C


# ──────────────────────────────────────────────
# 5. Model 2: Gaussian Puff (순간 누출)
# ──────────────────────────────────────────────

def gaussian_puff(
    X: np.ndarray, Y: np.ndarray,
    meteo: MeteoParams, src: SourceParams, sim: SimParams,
    t: float
) -> np.ndarray:
    """
    이동하는 가우시안 퍼프 농도 (g/m³)

    C(x,y,z,t) = Q_total / [(2π)^(3/2) σx σy σz]
                 × exp(-x_r²/2σx²)
                 × exp(-y_r²/2σy²)
                 × [exp(-(z-H)²/2σz²) + exp(-(z+H)²/2σz²)]
    여기서 퍼프 중심 = (u·t·cos, u·t·sin)
    """
    if t <= 0:
        return np.zeros_like(X)

    u = max(meteo.wind_speed, 0.5)
    cos_t, sin_t = wind_rotation(meteo.wind_dir_deg)

    # 퍼프 중심 이동
    xc = src.x0 + u * t * cos_t
    yc = src.y0 + u * t * sin_t

    # 퍼프에서의 상대 거리
    dx = X - xc
    dy = Y - yc

    # 이동거리 기준으로 σ 계산
    travel_dist = u * t
    sy = sigma_y(np.array([travel_dist]), meteo.stability)[0]
    sz = sigma_z(np.array([travel_dist]), meteo.stability)[0]
    sx = sy  # 풍하 방향 확산 ≈ 횡풍 방향

    sz = min(sz, meteo.mixing_height)
    H  = src.z0
    z  = sim.z_ref

    norm = (2 * np.pi)**1.5 * sx * sy * sz
    term_x = np.exp(-0.5 * (dx / sx)**2)
    term_y = np.exp(-0.5 * (dy / sy)**2)
    term_z = (np.exp(-0.5 * ((z - H) / sz)**2) +
              np.exp(-0.5 * ((z + H) / sz)**2))

    C = (src.Q_total / norm) * term_x * term_y * term_z
    return C


# ──────────────────────────────────────────────
# 6. Model 3: Dense Gas (ALOHA 방식, Britter-McQuaid)
# ──────────────────────────────────────────────

def dense_gas_britter_mcquaid(
    X: np.ndarray, Y: np.ndarray,
    meteo: MeteoParams, src: SourceParams, sim: SimParams,
    t: float
) -> np.ndarray:
    """
    Britter & McQuaid (1988) Dense Gas 모델
    - 무거운 가스의 중력 침강(gravity spreading) 효과 반영
    - ALOHA에서 채용하는 방식과 동일한 기본 구조

    적용 조건: ρ_gas / ρ_air > 1.1 이상
    출력 단위: g/m³ (ppm 변환은 convert_to_ppm 함수 사용)

    1) 부력 flux: g0' = g(ρg-ρa)/ρa × qv
    2) 연속 누출: plume width = f(g0', u, x)
    3) 순간 누출: cloud radius = f(g0, t)
    """
    rho_air = meteo.pressure * 0.02897 / (8.314 * meteo.temperature)  # kg/m³
    g = 9.81  # m/s²
    u = max(meteo.wind_speed, 0.5)

    # 초기 농도 (부피 분율)
    rho_g = src.density_gas
    C0_vol = rho_g / (rho_g + rho_air)   # 초기 부피 분율

    # 단위 부력 flux (m²/s³)
    g_prime0 = g * (rho_g - rho_air) / rho_air

    cos_t, sin_t = wind_rotation(meteo.wind_dir_deg)
    xw, yw = rotate_to_wind(X, Y, src.x0, src.y0, cos_t, sin_t)
    mask = xw > 0
    C = np.zeros_like(X)

    if not np.any(mask):
        return C

    xd = xw[mask]
    yd = yw[mask]

    if src.release_duration == 0:
        # ── 순간 누출 (Puff) ──
        # 이동 중심
        xc = u * t
        # 클라우드 반경: r(t) = r0 + α·(g_prime0·V0)^(1/4)·t^(1/2)
        V0 = np.pi * src.pool_radius**2 * 1.0  # 초기 체적 (1m 두께 가정)
        r0 = src.pool_radius
        r_t = r0 + 1.1 * (g_prime0 * V0)**0.25 * max(t, 0.1)**0.5

        # 클라우드 높이: h(t) = V0 / (π r²)
        h_t = max(V0 / (np.pi * r_t**2), 0.1)

        # Gaussian 농도 분포 내 클라우드
        dist_xr = xd - xc
        sigma_cloud = r_t / 2.15   # r_t ≈ 2.15σ
        C_vol = C0_vol * np.exp(-0.5 * (dist_xr / sigma_cloud)**2) \
                       * np.exp(-0.5 * (yd / sigma_cloud)**2)

    else:
        # ── 연속 누출 (Plume) ──
        # 풍하거리별 플룸 폭: b(x) = b0 × [1 + α(g_prime_x / u³)^(1/3)]
        qv = src.Q / (rho_g * 1000)  # 체적 유량 (m³/s)
        g_prime_line = g_prime0 * qv / u  # 단위 길이 부력

        b0 = src.pool_radius
        # Britter-McQuaid: b ∝ x^0.6 for gravity-dominated
        b_x = b0 + 2.5 * (g_prime_line / u**2)**0.333 * xd**0.6
        b_x = np.maximum(b_x, b0)

        # 플룸 높이 (중력 침강 → 낮게 유지)
        h_x = qv / (u * b_x)
        h_x = np.maximum(h_x, 0.1)

        # 횡풍 Gaussian × 풍하 top-hat 근사
        C_vol = C0_vol * (b0 / b_x) * np.exp(-0.5 * (yd / (b_x / 2))**2)

    # 부피 분율 → g/m³ 변환
    MW = src.molecular_weight
    C[mask] = C_vol * (MW * meteo.pressure) / (8.314 * meteo.temperature)  # g/m³

    return C


# ──────────────────────────────────────────────
# 7. 독성 임계값 (AEGL / IDLH)
# ──────────────────────────────────────────────

# 주요 HNS 물질 독성 기준 (ppm)
TOXICITY_LEVELS = {
    "Chlorine":    {"AEGL1": 0.5,  "AEGL2": 2.8,  "AEGL3": 50.0,  "IDLH": 10.0,   "MW": 71.0},
    "Ammonia":     {"AEGL1": 1.1,  "AEGL2": 16.0, "AEGL3": 50.0,  "IDLH": 300.0,  "MW": 17.0},
    "HCl":         {"AEGL1": 1.8,  "AEGL2": 22.0, "AEGL3": 100.0, "IDLH": 50.0,   "MW": 36.5},
    "Benzene":     {"AEGL1": 52.0, "AEGL2": 800.0,"AEGL3": 4000.0,"IDLH": 500.0,  "MW": 78.1},
    "H2S":         {"AEGL1": 0.51, "AEGL2": 17.0, "AEGL3": 50.0,  "IDLH": 50.0,   "MW": 34.1},
}


def gm3_to_ppm(C_gm3: np.ndarray, MW: float, T_K: float = 293.15, P_Pa: float = 101325) -> np.ndarray:
    """g/m³ → ppm 변환"""
    return C_gm3 * (8.314 * T_K) / (MW * P_Pa) * 1e6


# ──────────────────────────────────────────────
# 8. 애니메이션 생성
# ──────────────────────────────────────────────

def run_animation(
    model: Literal["plume", "puff", "dense_gas"],
    meteo: MeteoParams,
    src: SourceParams,
    sim: SimParams,
    substance: str = "Chlorine",
    save_path: Optional[str] = None,
    show: bool = True
):
    """
    시간별 대기확산 농도 애니메이션 생성

    Parameters
    ----------
    model       : 사용할 모델 ("plume" | "puff" | "dense_gas")
    save_path   : 저장 경로 (.gif 또는 .mp4), None이면 저장 안 함
    show        : plt.show() 호출 여부
    """
    MODEL_FUNC = {
        "plume":     gaussian_plume,
        "puff":      gaussian_puff,
        "dense_gas": dense_gas_britter_mcquaid,
    }
    assert model in MODEL_FUNC, f"지원하지 않는 모델: {model}"
    func = MODEL_FUNC[model]

    # 격자 생성
    x_lin = np.linspace(*sim.x_range, sim.nx)
    y_lin = np.linspace(*sim.y_range, sim.ny)
    X, Y = np.meshgrid(x_lin, y_lin)

    # 시간 배열
    times = np.arange(sim.t_start + sim.dt, sim.t_end + sim.dt, sim.dt)

    # 독성 기준
    tox = TOXICITY_LEVELS.get(substance, TOXICITY_LEVELS["Chlorine"])
    MW  = tox["MW"]

    # 농도 프레임 사전 계산
    print(f"[{model.upper()}] 농도 계산 중... ({len(times)} 프레임)")
    frames_C = []
    for t in times:
        C_gm3 = func(X, Y, meteo, src, sim, t)
        C_ppm = gm3_to_ppm(C_gm3, MW, meteo.temperature, meteo.pressure)
        frames_C.append(C_ppm)

    C_max_global = max(f.max() for f in frames_C)
    C_min_plot   = max(tox["AEGL1"] * 0.01, 1e-4)
    C_max_plot   = C_max_global * 1.0

    # ── 그림 설정 ──
    fig, ax = plt.subplots(figsize=(12, 7))
    fig.patch.set_facecolor("#0a1628")
    ax.set_facecolor("#0d1f3c")

    cmap = plt.cm.get_cmap("RdYlGn_r")
    cmap.set_under("#0d1f3c")

    im = ax.pcolormesh(
        X, Y, frames_C[0],
        cmap=cmap,
        norm=LogNorm(vmin=C_min_plot, vmax=C_max_plot),
        shading="auto"
    )

    # 독성 등고선
    AEGL_COLORS = {
        "AEGL1": ("#00ff88", f'AEGL-1 ({tox["AEGL1"]} ppm)'),
        "AEGL2": ("#ffcc00", f'AEGL-2 ({tox["AEGL2"]} ppm)'),
        "AEGL3": ("#ff4444", f'AEGL-3 ({tox["AEGL3"]} ppm)'),
    }

    contour_handles = {}
    for key, (color, label) in AEGL_COLORS.items():
        level = tox[key]
        if C_max_global >= level:
            cs = ax.contour(X, Y, frames_C[0], levels=[level],
                            colors=[color], linewidths=1.5, linestyles="--")
            contour_handles[key] = cs

    # 누출원 마커
    ax.plot(src.x0, src.y0, "w*", ms=14, zorder=10, label="누출원")

    # 컬러바
    cbar = plt.colorbar(im, ax=ax, fraction=0.03, pad=0.02)
    cbar.set_label("농도 (ppm)", color="white", fontsize=11)
    cbar.ax.yaxis.set_tick_params(color="white")
    plt.setp(cbar.ax.yaxis.get_ticklabels(), color="white")

    # 범례 패치
    from matplotlib.lines import Line2D
    legend_elements = [
        Line2D([0],[0], color=c, ls="--", lw=1.5, label=l)
        for _, (c, l) in AEGL_COLORS.items()
        if C_max_global >= tox[k.replace(c,"")[-5:]] if False else True
    ] + [Line2D([0],[0], marker="*", color="white", ms=10, ls="none", label="누출원")]
    ax.legend(handles=legend_elements, loc="upper right",
              facecolor="#1a2a4a", edgecolor="#446688", labelcolor="white", fontsize=9)

    # 축 설정
    ax.set_xlabel("X (m) - 동서", color="white")
    ax.set_ylabel("Y (m) - 남북", color="white")
    ax.tick_params(colors="white")
    for spine in ax.spines.values():
        spine.set_edgecolor("#446688")

    model_labels = {
        "plume":     "Gaussian Plume (연속누출)",
        "puff":      "Gaussian Puff (순간누출)",
        "dense_gas": "Dense Gas / ALOHA 방식",
    }
    title = ax.set_title(
        f"[WING] HNS 대기확산 - {model_labels[model]} | {substance} | t=0s",
        color="white", fontsize=12, fontweight="bold"
    )

    # 풍향 화살표
    cos_t, sin_t = wind_rotation(meteo.wind_dir_deg)
    ax_w = fig.add_axes([0.01, 0.88, 0.08, 0.08])
    ax_w.set_xlim(-1.5, 1.5)
    ax_w.set_ylim(-1.5, 1.5)
    ax_w.set_aspect("equal")
    ax_w.axis("off")
    ax_w.set_facecolor("#0a1628")
    ax_w.annotate("", xy=(cos_t, sin_t), xytext=(0, 0),
                  arrowprops=dict(arrowstyle="->", color="cyan", lw=2))
    ax_w.text(0, -1.4, f"{meteo.wind_speed}m/s\n{meteo.wind_dir_deg}°",
              color="cyan", fontsize=7, ha="center")

    def _update(frame_idx):
        C_ppm = frames_C[frame_idx]
        t_now = times[frame_idx]
        im.set_array(C_ppm.ravel())

        # 등고선 업데이트 (이전 제거 후 재생성)
        for coll in ax.collections[1:]:
            coll.remove()
        for key, (color, _) in AEGL_COLORS.items():
            level = tox[key]
            if C_ppm.max() >= level:
                ax.contour(X, Y, C_ppm, levels=[level],
                           colors=[color], linewidths=1.5, linestyles="--")

        title.set_text(
            f"[WING] HNS 대기확산 - {model_labels[model]} | {substance} | t={t_now:.0f}s"
        )
        return [im]

    ani = animation.FuncAnimation(
        fig, _update, frames=len(times), interval=300, blit=False
    )

    if save_path:
        ext = save_path.split(".")[-1].lower()
        print(f"애니메이션 저장 중: {save_path}")
        if ext == "gif":
            ani.save(save_path, writer="pillow", fps=5, dpi=120)
        else:
            ani.save(save_path, writer="ffmpeg", fps=5, dpi=120)
        print("저장 완료!")

    if show:
        plt.tight_layout()
        plt.show()

    plt.close(fig)
    return ani


# ──────────────────────────────────────────────
# 9. 다중 모델 비교 실행
# ──────────────────────────────────────────────

def run_all_models(
    meteo: MeteoParams,
    src: SourceParams,
    sim: SimParams,
    substance: str = "Chlorine",
    save_dir: str = "."
):
    """세 가지 모델 모두 GIF로 저장"""
    for model in ["plume", "puff", "dense_gas"]:
        path = f"{save_dir}/wing_hns_{model}_{substance.lower()}.gif"
        run_animation(model, meteo, src, sim, substance,
                      save_path=path, show=False)
        print(f"  ✓ {model} → {path}")


# ──────────────────────────────────────────────
# 10. 메인 실행 예시 (WING 기본값)
# ──────────────────────────────────────────────

if __name__ == "__main__":

    # ── 기상 조건 (서해 해상 기준)
    meteo = MeteoParams(
        wind_speed   = 6.0,       # m/s
        wind_dir_deg = 270.0,     # 서풍 (동쪽으로 이동)
        stability    = "D",       # 중립 (해상 대부분)
        temperature  = 288.15,    # 15°C
        mixing_height= 600.0,     # m
    )

    # ── 누출원 (선박 탱크 파손 시나리오)
    src = SourceParams(
        Q              = 20.0,    # g/s (연속 누출)
        Q_total        = 10000.0, # g   (순간 누출)
        z0             = 1.0,     # m (갑판 높이)
        release_duration = 300.0, # s
        molecular_weight = 71.0,  # Chlorine
        density_gas    = 3.17,    # kg/m³
        pool_radius    = 3.0,     # m
    )

    # ── 시뮬레이션 범위
    sim = SimParams(
        x_range = (-100, 4000),
        y_range = (-1000, 1000),
        nx=220, ny=160,
        t_end = 600.0,
        dt    = 30.0,
    )

    substance = "Chlorine"

    print("=" * 55)
    print("  WING HNS 대기확산 시뮬레이터")
    print(f"  물질: {substance} | 안정도: {meteo.stability}")
    print(f"  풍속: {meteo.wind_speed}m/s | 풍향: {meteo.wind_dir_deg}°")
    print("=" * 55)

    # 모델 선택 실행 (개별 실행 가능)
    # run_animation("plume",     meteo, src, sim, substance, save_path="wing_plume.gif")
    # run_animation("puff",      meteo, src, sim, substance, save_path="wing_puff.gif")
    # run_animation("dense_gas", meteo, src, sim, substance, save_path="wing_dense.gif")

    # 전체 모델 일괄 저장
    run_all_models(meteo, src, sim, substance, save_dir=".")
