# CelerisLab/tests/postproc/run_exp_ctrl_matrix_streakline.py
"""Streakline post-processing for exp_ctrl_matrix cases.

Release from step RELEASE_START; render snapshots at SNAPSHOT_STEPS.
Particles are cleared after each snapshot except the last so each frame
uses a fresh ~20k-step release window.
"""

from __future__ import annotations

import argparse
import json
import sys
from pathlib import Path

_REPO = Path(__file__).resolve().parents[2]

sys.path.insert(0, str(_REPO / "src"))
sys.path.insert(0, str(_REPO / "tests" / "postproc"))

import run_exp_ctrl_matrix_vorticity as vort
from CelerisLab import Simulation
from CelerisLab.common.preprocess import build_triangle_release_points, cylinders_from_triangle_layout
from CelerisLab.common.streakline import IntegratorConfig, ReleaseConfig, Streakline

DIAMETER_CELLS = vort.DIAMETER_CELLS
DEFAULT_OUT = _REPO / "tests" / "output" / "exp_ctrl_matrix_streak_nx1500"
RELEASE_START_STEP = 20_000
SNAPSHOT_STEPS = (40_000, 60_000, 80_000, 100_000)
CLEAR_AFTER_SNAPSHOT = frozenset({40_000, 60_000, 80_000})
STREAK_SAMPLE_EVERY = 50
STREAK_AGE_DECAY = 100_000.0
STREAK_BLUR_SIGMA = 0.8
STREAK_COLOR = (0.0, 0.35, 0.95)  # blue particles on white background


def _apply_body_actions(
    sim: Simulation, a1: float, a2: float, a3: float, u_lb: float
) -> None:
    w1 = vort._action_to_omega_lb(a1, u_lb)
    w2 = vort._action_to_omega_lb(a2, u_lb)
    w3 = vort._action_to_omega_lb(a3, u_lb)
    if vort.SWAP_ACTION23_BODIES:
        vort._set_body_omegas(sim, w1, w3, w2)
    else:
        vort._set_body_omegas(sim, w1, w2, w3)


def _render_streak(
    streak: Streakline,
    out_path: Path,
    *,
    step: int,
) -> dict:
    info = streak.render(
        str(out_path),
        age_decay_steps=STREAK_AGE_DECAY,
        blur_sigma=STREAK_BLUR_SIGMA,
        background_color=(1.0, 1.0, 1.0),
        streak_color=STREAK_COLOR,
    )
    info["step"] = int(step)
    info["n_particles"] = int(streak.n_particles)
    return info


def run_streak_case(
    case_id: str,
    slug: str,
    features: dict,
    *,
    out_dir: Path,
    total_steps: int,
    release_start: int,
    snapshot_steps: tuple[int, ...],
    sample_every: int,
    report_every: int,
    device_id: int,
) -> dict:
    compat = vort._ensure_compat_config(vort.CONFIG_PATH)
    sim = Simulation(compat, device_id=device_id)
    layout = vort._add_triangle_cylinders(sim)
    sim.initialize()
    u_lb = float(sim.lbm_cfg.velocity)
    dt_phys = (
        (vort.CYLINDER_DIAMETER_M / DIAMETER_CELLS)
        * (u_lb / vort.INLET_U_PHYS_M_S)
    )
    cylinders = cylinders_from_triangle_layout(layout)

    release_cfg = ReleaseConfig(
        mode="strip",
        line_count=1,
        line_span=0.0,
        downstream_count=5,
        downstream_spacing=1.0,
        inject_per_seed=1,
    )
    integrator_cfg = IntegratorConfig(
        alpha_t=0.25, alpha_x=0.40, max_particle_age=None
    )
    base_release = build_triangle_release_points(
        layout,
        nx=int(sim.lbm_cfg.nx),
        ny=int(sim.lbm_cfg.ny),
        diameter_cells=DIAMETER_CELLS,
    )

    streak = Streakline(
        release_points=base_release,
        release_cfg=release_cfg,
        integrator_cfg=integrator_cfg,
        nx=int(sim.lbm_cfg.nx),
        ny=int(sim.lbm_cfg.ny),
        cylinders=cylinders,
    )

    snapshot_set = set(snapshot_steps)
    print(
        f"--- {case_id} {slug} steps={total_steps} release_from={release_start} "
        f"snapshots={list(snapshot_steps)} device={device_id} ---"
    )

    snapshots: list[dict] = []

    for step in range(total_steps):
        t_phys = step * dt_phys
        a1, a2, a3 = vort._actions_at_time(t_phys, features)
        _apply_body_actions(sim, a1, a2, a3, u_lb)
        sim.run(1)

        current_step = step + 1
        observed = False
        if step >= release_start and current_step % sample_every == 0:
            macro = sim.get_macroscopic()
            streak.observe(ux=macro["ux"], uy=macro["uy"], step=current_step)
            observed = True

        if current_step in snapshot_set:
            if streak.n_particles == 0:
                raise RuntimeError(
                    f"{case_id}: no particles at step {current_step}; "
                    f"check release_start/sample_every."
                )
            png = out_dir / f"streakline_{case_id}_{slug}_step{current_step:06d}.png"
            snap_info = _render_streak(streak, png, step=current_step)
            snap_info["image_path"] = str(png)
            snapshots.append(snap_info)
            print(
                f"  snapshot step {current_step} particles={streak.n_particles} "
                f"-> {png.name}"
            )

            if current_step in CLEAR_AFTER_SNAPSHOT and current_step < total_steps:
                streak.reset()
                if observed:
                    macro = sim.get_macroscopic()
                    streak.observe(ux=macro["ux"], uy=macro["uy"], step=current_step)

        if report_every > 0 and current_step % report_every == 0:
            print(
                f"  step {current_step}/{total_steps} "
                f"a=({a1:+.5f},{a2:+.5f},{a3:+.5f}) particles={streak.n_particles}"
            )

    sim.close()

    summary = {
        "case_id": case_id,
        "slug": slug,
        "total_steps": int(total_steps),
        "release_start_step": int(release_start),
        "snapshot_steps": list(snapshot_steps),
        "clear_after_snapshot": sorted(CLEAR_AFTER_SNAPSHOT),
        "sample_every": int(sample_every),
        "device_id": int(device_id),
        "release_points_dense": int(base_release.shape[0]),
        "snapshots": snapshots,
        "swap_action23_bodies": bool(vort.SWAP_ACTION23_BODIES),
    }
    with (out_dir / f"summary_{case_id}_{slug}.json").open("w", encoding="utf-8") as f:
        json.dump(summary, f, indent=2)
    return summary


def main() -> int:
    ap = argparse.ArgumentParser(description="exp_ctrl_matrix streakline batch")
    ap.add_argument("--out-dir", type=str, default=str(DEFAULT_OUT))
    ap.add_argument("--steps", type=int, default=vort.FIXED_STEPS)
    ap.add_argument("--release-start", type=int, default=RELEASE_START_STEP)
    ap.add_argument(
        "--snapshots",
        type=str,
        default=",".join(str(s) for s in SNAPSHOT_STEPS),
        help="Comma-separated render steps, e.g. 40000,60000,100000",
    )
    ap.add_argument("--sample-every", type=int, default=STREAK_SAMPLE_EVERY)
    ap.add_argument("--report-every", type=int, default=20000)
    ap.add_argument("--device-id", type=int, default=2)
    ap.add_argument("--cases", type=str, default="")
    args = ap.parse_args()

    snapshot_steps = tuple(
        int(s.strip()) for s in args.snapshots.split(",") if s.strip()
    )
    out_dir = Path(args.out_dir)
    out_dir.mkdir(parents=True, exist_ok=True)
    selected = (
        {c.strip() for c in args.cases.split(",") if c.strip()}
        if args.cases
        else None
    )

    with vort.CONFIG_PATH.open("r", encoding="utf-8") as f:
        grid = json.load(f)["grid"]
    print(
        f"Output: {out_dir} | grid={grid['nx']}x{grid['ny']} | steps={args.steps} | "
        f"release from {args.release_start} | snapshots={list(snapshot_steps)} | "
        f"device={args.device_id}"
    )

    summaries = []
    for case_id, slug, features in vort.CONTROL_CASES:
        if selected and case_id not in selected:
            continue
        summaries.append(
            run_streak_case(
                case_id,
                slug,
                features,
                out_dir=out_dir,
                total_steps=int(args.steps),
                release_start=int(args.release_start),
                snapshot_steps=snapshot_steps,
                sample_every=int(args.sample_every),
                report_every=int(args.report_every),
                device_id=int(args.device_id),
            )
        )

    manifest = {
        "grid": grid,
        "steps": int(args.steps),
        "release_start_step": int(args.release_start),
        "snapshot_steps": list(snapshot_steps),
        "clear_after_snapshot": sorted(CLEAR_AFTER_SNAPSHOT),
        "sample_every": int(args.sample_every),
        "streak_color": list(STREAK_COLOR),
        "stealth_steady_omega_m_s": float(vort.STEALTH_STEADY_OMEGA_M_S),
        "device_id": int(args.device_id),
        "swap_action23_bodies": bool(vort.SWAP_ACTION23_BODIES),
        "cases": summaries,
    }
    manifest_path = out_dir / "manifest.json"
    with manifest_path.open("w", encoding="utf-8") as f:
        json.dump(manifest, f, indent=2)
    print(f"Manifest: {manifest_path}")
    return 0


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