# CelerisLab/common/streakline/_config.py
"""
Configuration dataclasses for streakline particle integration.
"""

from __future__ import annotations

from dataclasses import dataclass
from typing import Optional

import numpy as np

from .._types import CylinderSpec


@dataclass(frozen=True)
class FlowFrame:
    """One sampled velocity frame (internal use only)."""
    step: int
    ux: np.ndarray
    uy: np.ndarray


@dataclass(frozen=True)
class ReleaseConfig:
    """Release strategy configuration for dense streakline seeding.

    Args:
        mode: ``"point"`` | ``"line"`` | ``"strip"``.
        line_span: Total cross-stream span for ``line`` / ``strip`` modes.
        line_count: Number of cross-stream release points.
        downstream_count: Number of streamwise points (``strip`` mode only).
        downstream_spacing: Spacing between downstream copies (lattice units).
        inject_per_seed: Particles injected per release point each frame.
    """
    mode: str = "strip"
    line_span: float = 12.0
    line_count: int = 5
    downstream_count: int = 4
    downstream_spacing: float = 3.0
    inject_per_seed: int = 2


@dataclass(frozen=True)
class IntegratorConfig:
    """Particle integration and filtering settings.

    Args:
        alpha_t: Fraction of frame interval used as substep ceiling.
        alpha_x: Fraction of lattice unit per substep (CFL-like).
        age_decay_steps: Exponential decay scale for age-weight in rendering.
        max_particle_age: Hard cutoff age (None = no cutoff).
        diffusion_coeff: Random-walk diffusion for visual thickening (0 = off).
        random_seed: RNG seed for diffusion.
    """
    alpha_t: float = 0.2
    alpha_x: float = 0.4
    age_decay_steps: float = 20000.0
    max_particle_age: Optional[float] = None
    diffusion_coeff: float = 0.0
    random_seed: int = 1234


def build_release_points(
    base_points: np.ndarray,
    release_cfg: ReleaseConfig,
) -> np.ndarray:
    """Expand *base_points* to a dense release grid per *release_cfg*."""
    pts = np.asarray(base_points, dtype=np.float64)
    if pts.ndim != 2 or pts.shape[1] != 2:
        raise ValueError("base_points must be [N,2].")
    mode = str(release_cfg.mode).lower()
    if mode not in ("point", "line", "strip"):
        raise ValueError("release mode must be point|line|strip.")

    line_count = max(1, int(release_cfg.line_count))
    ds_count = max(1, int(release_cfg.downstream_count))
    line_offsets = np.linspace(
        -0.5 * float(release_cfg.line_span),
        0.5 * float(release_cfg.line_span),
        line_count,
    )
    ds_offsets = np.arange(ds_count, dtype=np.float64) * float(
        release_cfg.downstream_spacing
    )

    out: list[list[float]] = []
    for x0, y0 in pts:
        line_iter = [0.0] if mode == "point" else line_offsets
        ds_iter = [0.0] if mode in ("point", "line") else ds_offsets
        for dy in line_iter:
            for dx in ds_iter:
                out.append([float(x0 + dx), float(y0 + dy)])
    return np.asarray(out, dtype=np.float64)
