# CelerisLab/tests/postproc/run_kan99b_streakline.py
"""Kan99b streakline demo using the new Streakline class (online mode only).

Usage::

    python tests/run_kan99b_streakline.py --domain M --re 100 --alpha 1.0
"""

from __future__ import annotations

import argparse
import json
import os
import tempfile
from dataclasses import dataclass
from typing import Tuple

import numpy as np

from CelerisLab import Simulation
from CelerisLab.common.streakline import Streakline, ReleaseConfig, IntegratorConfig

_REPO = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", ".."))
_DEFAULT_LBM = os.path.join(_REPO, "src", "CelerisLab", "configs", "config_lbm.json")

U_INF = 0.03
D_LATTICE = 30.0
R_LATTICE = 15.0
KAN99B_ST_REF = 0.1655


@dataclass(frozen=True)
class DomainSpec:
    key: str
    nx: int
    ny: int
    center: Tuple[float, float]


def _domain_specs() -> dict:
    return {
        "S": DomainSpec("S", 1081, 481, (360.0, 240.0)),
        "M": DomainSpec("M", 1351, 601, (450.0, 300.0)),
        "L": DomainSpec("L", 1801, 721, (600.0, 360.0)),
    }


def _load_json(path: str) -> dict:
    with open(path, "r", encoding="utf-8") as f:
        return json.load(f)


def _write_json(path: str, payload: dict) -> None:
    with open(path, "w", encoding="utf-8") as f:
        json.dump(payload, f, indent=2)


def _nu_from_re(reynolds: float) -> float:
    return U_INF * D_LATTICE / float(reynolds)


def _omega_body(alpha: float) -> float:
    return 2.0 * float(alpha) * U_INF / D_LATTICE


def _build_cfg(base_cfg: dict, *, nx: int, ny: int, re: float, inlet_scheme: str) -> dict:
    cfg = json.loads(json.dumps(base_cfg))
    cfg["grid"]["nx"] = int(nx)
    cfg["grid"]["ny"] = int(ny)
    cfg["grid"]["nz"] = 1
    cfg["physics"]["velocity"] = float(U_INF)
    cfg["physics"]["viscosity"] = float(_nu_from_re(re))
    cfg["physics"]["rho"] = 1.0
    cfg["method"]["collision"] = "MRT"
    cfg["method"]["streaming"] = "double_buffer"
    cfg["method"]["store_precision"] = "FP32"
    cfg["method"]["ddf_shifting"] = False
    cfg["method"]["les"]["enabled"] = False
    cfg["method"]["inlet"]["profile"] = "uniform"
    cfg["method"]["inlet"]["scheme"] = str(inlet_scheme)
    cfg["method"]["outlet"]["mode"] = "neq_extrap"
    cfg["method"]["y_wall_bc"] = "free_slip"
    return cfg


def _build_simulation(
    *, domain: DomainSpec, re: float, alpha: float, inlet_scheme: str
) -> Simulation:
    base_cfg = _load_json(_DEFAULT_LBM)
    cfg = _build_cfg(
        base_cfg, nx=domain.nx, ny=domain.ny, re=re, inlet_scheme=inlet_scheme
    )
    body_doc = {
        "objects": [
            {
                "type": "cylinder",
                "center": [float(domain.center[0]), float(domain.center[1])],
                "radius": float(R_LATTICE),
                "omega": float(_omega_body(alpha)),
            }
        ]
    }
    tmpd = tempfile.mkdtemp(prefix="celeris_streakline_")
    lbm_tmp = os.path.join(tmpd, "config_lbm.json")
    body_tmp = os.path.join(tmpd, "config_body.json")
    _write_json(lbm_tmp, cfg)
    _write_json(body_tmp, body_doc)
    sim = Simulation(lbm_config_path=lbm_tmp, body_config_path=body_tmp)
    sim.bodies.get(0).state.omega = np.float32(_omega_body(alpha))
    sim.initialize()
    return sim


def _default_base_release_points(center: Tuple[float, float]) -> np.ndarray:
    x_rel = float(center[0] - 6.0 * D_LATTICE)
    y0 = float(center[1])
    return np.array(
        [
            [x_rel, y0 - 18.0],
            [x_rel, y0 - 6.0],
            [x_rel, y0 + 6.0],
            [x_rel, y0 + 18.0],
        ],
        dtype=np.float64,
    )


# ---- Sampling plan helper (kept as a local utility, not part of the library) ----
def _estimate_sampling_plan(
    *,
    st_ref: float,
    diameter: float,
    u_ref: float,
    snapshots_per_period: float = 24.0,
    periods: int = 5,
) -> dict:
    period_steps = float(diameter) / (float(st_ref) * float(u_ref))
    save_every = int(
        max(20, round(period_steps / snapshots_per_period / 10.0) * 10)
    )
    n_snapshots = int(max(20, round(float(periods) * float(snapshots_per_period))))
    return {
        "st_ref": float(st_ref),
        "period_steps_est": float(period_steps),
        "save_every_recommended": int(save_every),
        "snapshot_count_recommended": int(n_snapshots),
    }


def main() -> int:
    ap = argparse.ArgumentParser(description="Kan99b streakline demo")
    ap.add_argument("--domain", default="M", choices=("S", "M", "L"))
    ap.add_argument("--re", type=float, default=100.0)
    ap.add_argument("--alpha", type=float, default=1.0)
    ap.add_argument("--inlet-scheme", default="regularized",
                    choices=("regularized", "zou_he_local"))
    ap.add_argument("--start-step", type=int, default=60_000)
    ap.add_argument("--sample-every", type=int, default=0,
                    help="0 uses recommended value.")
    ap.add_argument("--n-snapshots", type=int, default=0,
                    help="0 uses recommended value.")
    ap.add_argument("--release-mode", default="strip",
                    choices=("point", "line", "strip"))
    ap.add_argument("--line-span", type=float, default=0.0)
    ap.add_argument("--line-count", type=int, default=1)
    ap.add_argument("--downstream-count", type=int, default=5)
    ap.add_argument("--downstream-spacing", type=float, default=1.0)
    ap.add_argument("--inject-per-seed", type=int, default=2)
    ap.add_argument("--alpha-t", type=float, default=0.2)
    ap.add_argument("--alpha-x", type=float, default=0.4)
    ap.add_argument("--diffusion-coeff", type=float, default=0.0)
    ap.add_argument(
        "--out-dir",
        type=str,
        default=os.path.join(
            _REPO, "tests", "output", "streakline", "kan99b_k2"
        ),
    )
    args = ap.parse_args()

    domain = _domain_specs()[args.domain]
    out_dir = os.path.abspath(args.out_dir)
    os.makedirs(out_dir, exist_ok=True)

    plan = _estimate_sampling_plan(
        st_ref=KAN99B_ST_REF, diameter=D_LATTICE, u_ref=U_INF
    )
    sample_every = (
        int(args.sample_every)
        if int(args.sample_every) > 0
        else int(plan["save_every_recommended"])
    )
    n_snapshots = (
        int(args.n_snapshots)
        if int(args.n_snapshots) > 0
        else int(plan["snapshot_count_recommended"])
    )

    release_cfg = ReleaseConfig(
        mode=args.release_mode,
        line_span=float(args.line_span),
        line_count=max(1, int(args.line_count)),
        downstream_count=max(1, int(args.downstream_count)),
        downstream_spacing=float(args.downstream_spacing),
        inject_per_seed=max(1, int(args.inject_per_seed)),
    )
    integrator_cfg = IntegratorConfig(
        alpha_t=float(args.alpha_t),
        alpha_x=float(args.alpha_x),
        diffusion_coeff=float(args.diffusion_coeff),
    )
    base_release = _default_base_release_points(domain.center)

    streak = Streakline(
        release_points=base_release,
        release_cfg=release_cfg,
        integrator_cfg=integrator_cfg,
        nx=domain.nx,
        ny=domain.ny,
        cylinders=[(domain.center, R_LATTICE)],
    )

    sim = _build_simulation(
        domain=domain,
        re=float(args.re),
        alpha=float(args.alpha),
        inlet_scheme=args.inlet_scheme,
    )

    # Burn-in phase: step the simulation but don't feed streakline
    print(f"Burning-in {args.start_step} steps ...")
    sim.run(int(args.start_step))

    # Sampling phase: step and feed velocity frames to streakline
    target_last = int(args.start_step) + sample_every * (n_snapshots - 1)
    frames_collected = 0
    print(
        f"Sampling every {sample_every} steps for {n_snapshots} frames "
        f"(up to step {target_last})..."
    )
    while int(sim.stepper.step_count) < target_last:
        sim.step(1)
        step = int(sim.stepper.step_count)
        if (step - int(args.start_step)) % sample_every != 0:
            continue
        macro = sim.get_macroscopic()
        streak.observe(ux=macro["ux"], uy=macro["uy"], step=step)
        frames_collected += 1
        if frames_collected >= n_snapshots:
            break

    sim.close()

    render_info = streak.render(
        os.path.join(out_dir, "streakline.png"),
        age_decay_steps=integrator_cfg.age_decay_steps,
        blur_sigma=1.2,
    )

    meta = {
        "case": {
            "domain": args.domain,
            "re": float(args.re),
            "alpha": float(args.alpha),
            "inlet_scheme": args.inlet_scheme,
            "collision": "MRT",
        },
        "sampling_estimate": plan,
        "sampling_used": {
            "start_step": int(args.start_step),
            "sample_every": int(sample_every),
            "n_snapshots": int(n_snapshots),
            "frames_collected": frames_collected,
        },
        "release": {
            "config": {
                "mode": args.release_mode,
                "line_span": float(args.line_span),
                "line_count": max(1, int(args.line_count)),
                "downstream_count": max(1, int(args.downstream_count)),
                "downstream_spacing": float(args.downstream_spacing),
                "inject_per_seed": max(1, int(args.inject_per_seed)),
            },
            "base_points": base_release.tolist(),
        },
        "diagnostics": {"n_particles_final": int(streak.n_particles)},
        "render": render_info,
    }
    _write_json(os.path.join(out_dir, "streakline_meta.json"), meta)

    print(f"Recommended sample_every: {plan['save_every_recommended']} steps")
    print(f"Recommended snapshots: {plan['snapshot_count_recommended']}")
    print(f"Frames collected: {frames_collected}")
    print(f"Final particles: {streak.n_particles}")
    print(f"Output image: {render_info['image_path']}")
    return 0


if __name__ == "__main__":
    raise SystemExit(main())
